(() => { 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; let companionConnected = false; let companionDocumentOpen = false; let currentScanStatus = document.querySelector("[data-onboarding-scan-panel]")?.dataset.scanStatus || "NotStarted"; let scanStartedInThisPage = currentScanStatus === "Running"; 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 documentOpen = !!field(status, "documentOpen", "DocumentOpen"); const version = field(status, "companionVersion", "CompanionVersion") || "Waiting"; const documentName = field(status, "currentDocumentName", "CurrentDocumentName") || "No document reported yet"; const label = connected ? "Connected" : labelFor(status); companionConnected = connected; companionDocumentOpen = documentOpen; 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) => { if (!connected) { node.textContent = "Open Word and start the Companion. This page will update automatically."; } else if (!documentOpen) { node.textContent = "Open your manuscript in Word, then the scan button will appear."; } else if (currentScanStatus === "Complete") { node.textContent = "Your scan is ready. Review what PlotDirector found before anything is imported."; } else if (currentScanStatus === "Running") { node.textContent = "The Companion is reading your manuscript and sending structure back to PlotDirector."; } else { node.textContent = "Your Companion is connected. Scan your manuscript when you are ready."; } }); 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."; }); updateScanButton(); }; const numberText = (value) => { const number = Number.parseInt(value || "0", 10); return Number.isInteger(number) ? number.toLocaleString() : "0"; }; const updateScanButton = () => { document.querySelectorAll("[data-onboarding-scan-start]").forEach((button) => { const openingReview = currentScanStatus === "Complete" && scanStartedInThisPage; button.disabled = openingReview || !companionConnected || !companionDocumentOpen || currentScanStatus === "Running"; button.textContent = openingReview ? "Opening chapter review..." : currentScanStatus === "Complete" ? "Scan again" : "Scan manuscript"; }); }; const applyScanState = (state) => { if (!state) { return; } const status = field(state, "status", "Status") || "NotStarted"; if (status === "Running") { scanStartedInThisPage = true; } currentScanStatus = status; const autoOpeningReview = status === "Complete" && scanStartedInThisPage; let message = field(state, "message", "Message") || (status === "Complete" ? "Scan complete. Review what PlotDirector found." : "Ready to scan"); if (autoOpeningReview) { message = "Scan complete. Opening chapter review..."; } const percent = field(state, "percentComplete", "PercentComplete"); const previewId = field(state, "previewID", "PreviewID") || field(state, "previewId", "PreviewId"); const panel = document.querySelector("[data-onboarding-scan-panel]"); if (panel) { panel.dataset.scanStatus = status; panel.classList.toggle("is-running", status === "Running"); panel.classList.toggle("is-complete", status === "Complete"); panel.classList.toggle("is-failed", status === "Failed"); } document.querySelectorAll("[data-onboarding-scan-message]").forEach((node) => { node.textContent = message; }); document.querySelectorAll("[data-onboarding-scan-percent]").forEach((node) => { node.textContent = percent === null || percent === undefined ? "" : `${percent}%`; }); document.querySelectorAll("[data-onboarding-scan-progress]").forEach((node) => { node.style.width = `${Math.max(0, Math.min(100, Number.parseInt(percent || "0", 10) || 0))}%`; }); document.querySelectorAll("[data-onboarding-scan-chapters]").forEach((node) => { node.textContent = numberText(field(state, "chapterCount", "ChapterCount")); }); document.querySelectorAll("[data-onboarding-scan-scenes]").forEach((node) => { node.textContent = numberText(field(state, "sceneCount", "SceneCount")); }); document.querySelectorAll("[data-onboarding-scan-characters]").forEach((node) => { node.textContent = numberText(field(state, "characterCandidateCount", "CharacterCandidateCount")); }); document.querySelectorAll("[data-onboarding-scan-words]").forEach((node) => { node.textContent = numberText(field(state, "totalWordCount", "TotalWordCount")); }); document.querySelectorAll("[data-onboarding-scan-review]").forEach((node) => { const ready = status === "Complete" && previewId; node.classList.toggle("disabled", !ready); node.setAttribute("aria-disabled", String(!ready)); node.href = ready ? `/onboarding/scan-review?previewId=${encodeURIComponent(previewId)}` : "#"; }); updateScanButton(); if (status === "Complete" && previewId && document.querySelector("[data-onboarding-auto-review]") && scanStartedInThisPage && !document.body.dataset.onboardingReviewRedirected) { document.body.dataset.onboardingReviewRedirected = "true"; window.location.href = `/onboarding/scan-review?previewId=${encodeURIComponent(previewId)}`; } }; const connection = new signalR.HubConnectionBuilder() .withUrl("/hubs/word-companion-follow") .withAutomaticReconnect() .build(); let pendingBuildPreviewId = ""; let pendingManualSaveBuildId = 0; let pendingManualSavePreviewId = ""; const setBuildActionState = (text, disabled, mode = "") => { document.querySelectorAll("[data-onboarding-build-action]").forEach((button) => { if (!button.dataset.defaultActionText) { button.dataset.defaultActionText = button.textContent.trim() || "Detect Scenes"; } button.textContent = text; button.disabled = disabled; if (mode) { button.dataset.buildMode = mode; } else { delete button.dataset.buildMode; } }); }; const setManualSaveRequired = (state) => { const buildId = Number.parseInt( field(state, "buildID", "BuildID") ?? field(state, "buildId", "BuildId") ?? "0", 10); const previewId = field(state, "previewID", "PreviewID") || field(state, "previewId", "PreviewId") || pendingBuildPreviewId; const message = field(state, "message", "Message") || "Scene markers have been added. Save the document in Word, then confirm here."; pendingManualSaveBuildId = Number.isInteger(buildId) ? buildId : 0; pendingManualSavePreviewId = previewId || pendingBuildPreviewId; pendingBuildPreviewId = pendingManualSavePreviewId || pendingBuildPreviewId; document.querySelectorAll("[data-onboarding-build-panel]").forEach((node) => { node.classList.add("is-running", "is-manual-save"); }); document.querySelectorAll("[data-onboarding-build-message]").forEach((node) => { node.textContent = message; }); document.querySelectorAll("[data-onboarding-build-percent]").forEach((node) => { node.textContent = "90%"; }); document.querySelectorAll("[data-onboarding-build-progress]").forEach((node) => { node.style.width = "90%"; }); document.querySelectorAll("[data-onboarding-manual-save-actions]").forEach((node) => { node.hidden = false; }); document.querySelectorAll("[data-onboarding-build-save-confirm]").forEach((button) => { button.disabled = !pendingManualSaveBuildId; button.textContent = "I've saved the Word document"; }); setBuildActionState("I've saved the Word document", !pendingManualSaveBuildId, "confirm-save"); }; const clearManualSaveRequired = () => { pendingManualSaveBuildId = 0; pendingManualSavePreviewId = ""; document.querySelectorAll("[data-onboarding-build-panel]").forEach((node) => { node.classList.remove("is-manual-save"); }); document.querySelectorAll("[data-onboarding-manual-save-actions]").forEach((node) => { node.hidden = true; }); document.querySelectorAll("[data-onboarding-build-save-confirm]").forEach((button) => { button.disabled = true; button.textContent = "I've saved the Word document"; }); document.querySelectorAll("[data-onboarding-build-action]").forEach((button) => { const defaultText = button.dataset.defaultActionText || button.textContent.trim() || "Detect Scenes"; button.textContent = defaultText; button.disabled = false; delete button.dataset.buildMode; }); }; const confirmManualSaveFromWeb = async () => { if (!pendingManualSaveBuildId) { return; } setBuildActionState("Confirming...", true, "confirm-save"); document.querySelectorAll("[data-onboarding-build-save-confirm]").forEach((button) => { button.disabled = true; button.textContent = "Confirming..."; }); try { await connection.invoke("CompleteOnboardingBuildMarkerSync", pendingManualSaveBuildId, { previewID: pendingManualSavePreviewId || pendingBuildPreviewId || null, markerStatus: "UserConfirmedSave", message: "Scene markers added; user confirmed the Word document was saved.", markersChanged: true, saveState: "UserConfirmedSave", saveApi: "manual-web" }); } catch (error) { document.querySelectorAll("[data-onboarding-build-message]").forEach((node) => { node.textContent = error?.message || "PlotDirector could not confirm the save yet. Keep Word open and try again."; }); document.querySelectorAll("[data-onboarding-build-save-confirm]").forEach((button) => { button.disabled = false; button.textContent = "I've saved the Word document"; }); setBuildActionState("I've saved the Word document", false, "confirm-save"); } }; connection.on("WordCompanionPresenceChanged", applyStatus); connection.on("OnboardingScanStateChanged", applyScanState); connection.on("OnboardingScanProgress", applyScanState); connection.on("OnboardingScanCompleted", applyScanState); connection.on("OnboardingScanFailed", applyScanState); connection.on("OnboardingBuildProgress", (state) => { const message = field(state, "message", "Message") || "Preparing chapters..."; const percent = field(state, "percentComplete", "PercentComplete"); const safePercent = Math.max(0, Math.min(100, Number.parseInt(percent || "0", 10) || 0)); const markerStatus = field(state, "markerStatus", "MarkerStatus"); if (markerStatus === "UserSaveRequired") { setManualSaveRequired(state); return; } document.querySelectorAll("[data-onboarding-build-panel]").forEach((node) => { node.classList.add("is-running"); node.classList.remove("is-manual-save"); }); document.querySelectorAll("[data-onboarding-build-message]").forEach((node) => { node.textContent = message; }); document.querySelectorAll("[data-onboarding-build-percent]").forEach((node) => { node.textContent = percent === null || percent === undefined ? "" : `${percent}%`; }); document.querySelectorAll("[data-onboarding-build-progress]").forEach((node) => { node.style.width = `${safePercent}%`; }); }); connection.on("OnboardingBuildCompleted", (result) => { const previewId = field(result, "previewID", "PreviewID") || field(result, "previewId", "PreviewId"); if (previewId) { window.location.href = `/onboarding/build-complete?previewId=${encodeURIComponent(previewId)}`; } }); connection.on("OnboardingBuildManualSaveRequired", setManualSaveRequired); connection.on("OnboardingBuildMarkerSyncCompleted", (result) => { const previewId = field(result, "previewID", "PreviewID") || field(result, "previewId", "PreviewId"); pendingBuildPreviewId = previewId || pendingBuildPreviewId || pendingManualSavePreviewId; clearManualSaveRequired(); if (pendingBuildPreviewId) { window.location.href = `/onboarding/build-complete?previewId=${encodeURIComponent(pendingBuildPreviewId)}`; } }); 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 })); document.querySelectorAll("[data-onboarding-scan-start]").forEach((button) => { button.addEventListener("click", async () => { button.disabled = true; applyScanState({ status: "Running", message: "Asking the Word Companion to scan...", percentComplete: 0 }); try { const state = await connection.invoke("StartOnboardingManuscriptScan"); applyScanState(state); } catch (error) { applyScanState({ status: "Failed", message: error?.message || "The scan could not start. Reconnect the Word Companion and try again." }); } }); }); document.querySelectorAll("[data-onboarding-build-start]").forEach((button) => { button.addEventListener("click", async () => { if (button.dataset.buildMode === "confirm-save" || pendingManualSaveBuildId) { await confirmManualSaveFromWeb(); return; } const previewId = button.dataset.previewId; if (!previewId) { return; } pendingBuildPreviewId = previewId; clearManualSaveRequired(); button.disabled = true; document.querySelectorAll("[data-onboarding-build-panel]").forEach((node) => { node.classList.add("is-running"); }); document.querySelectorAll("[data-onboarding-build-message]").forEach((node) => { node.textContent = "Preparing approved chapters for scene detection..."; }); document.querySelectorAll("[data-onboarding-build-percent]").forEach((node) => { node.textContent = "0%"; }); document.querySelectorAll("[data-onboarding-build-progress]").forEach((node) => { node.style.width = "0%"; }); try { const result = await connection.invoke("StartOnboardingProjectBuild", previewId); pendingBuildPreviewId = field(result, "previewID", "PreviewID") || previewId; document.querySelectorAll("[data-onboarding-build-message]").forEach((node) => { node.textContent = "Updating Word document with scene markers. Keep Word open, then save the document when prompted."; }); } catch (error) { button.disabled = false; document.querySelectorAll("[data-onboarding-build-panel]").forEach((node) => { node.classList.remove("is-running"); }); document.querySelectorAll("[data-onboarding-build-message]").forEach((node) => { node.textContent = error?.message || "The approved chapters could not be prepared. Check the review and try again."; }); document.querySelectorAll("[data-onboarding-build-percent]").forEach((node) => { node.textContent = ""; }); } }); }); document.querySelectorAll("[data-onboarding-build-save-confirm]").forEach((button) => { button.addEventListener("click", confirmManualSaveFromWeb); }); })();