diff --git a/PlotLine/Controllers/WordCompanionHostController.cs b/PlotLine/Controllers/WordCompanionHostController.cs index 7827974..39ec67e 100644 --- a/PlotLine/Controllers/WordCompanionHostController.cs +++ b/PlotLine/Controllers/WordCompanionHostController.cs @@ -19,6 +19,7 @@ public sealed class WordCompanionHostController(ICurrentUserService currentUser, { IsAuthenticated = currentUser.IsAuthenticated, IsAdmin = isAdmin, + UserID = currentUser.UserId, DisplayName = currentUser.DisplayName, LoginUrl = Url.Action("Login", "Account", new { returnUrl = "/word-companion" }) ?? "/Account/Login?returnUrl=%2Fword-companion" }); diff --git a/PlotLine/Hubs/WordCompanionFollowHub.cs b/PlotLine/Hubs/WordCompanionFollowHub.cs index 2b1b662..315db79 100644 --- a/PlotLine/Hubs/WordCompanionFollowHub.cs +++ b/PlotLine/Hubs/WordCompanionFollowHub.cs @@ -1,9 +1,68 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.SignalR; +using PlotLine.Models; +using PlotLine.Services; +using System.Security.Claims; namespace PlotLine.Hubs; [Authorize] -public sealed class WordCompanionFollowHub : Hub +public sealed class WordCompanionFollowHub(IWordCompanionPresenceService presence) : Hub { + public override async Task OnConnectedAsync() + { + if (TryGetUserId(out var userId)) + { + await Groups.AddToGroupAsync(Context.ConnectionId, PresenceGroup(userId)); + } + + await base.OnConnectedAsync(); + } + + public async Task WatchCompanionPresence() + { + var userId = RequireUserId(); + await Groups.AddToGroupAsync(Context.ConnectionId, PresenceGroup(userId)); + return await presence.GetStatusAsync(userId); + } + + public async Task RegisterCompanion(WordCompanionPresenceRegistration registration) + { + var userId = RequireUserId(); + if (registration.UserID.HasValue && registration.UserID.Value != userId) + { + throw new HubException("The Companion is signed in as a different user."); + } + + var status = await presence.RegisterAsync(userId, Context.ConnectionId, registration); + await Clients.Group(PresenceGroup(userId)).SendAsync("WordCompanionPresenceChanged", status); + return status; + } + + public async Task CompanionHeartbeat(WordCompanionPresenceHeartbeat heartbeat) + { + var userId = RequireUserId(); + var status = await presence.HeartbeatAsync(userId, Context.ConnectionId, heartbeat); + await Clients.Group(PresenceGroup(userId)).SendAsync("WordCompanionPresenceChanged", status); + return status; + } + + public override async Task OnDisconnectedAsync(Exception? exception) + { + var status = await presence.DisconnectAsync(Context.ConnectionId); + if (status is not null) + { + await Clients.Group(PresenceGroup(status.UserID)).SendAsync("WordCompanionPresenceChanged", status); + } + + await base.OnDisconnectedAsync(exception); + } + + private int RequireUserId() + => TryGetUserId(out var userId) ? userId : throw new HubException("Sign in to use the Word Companion."); + + private bool TryGetUserId(out int userId) + => int.TryParse(Context.User?.FindFirstValue(ClaimTypes.NameIdentifier), out userId); + + private static string PresenceGroup(int userId) => $"word-companion-presence:{userId}"; } diff --git a/PlotLine/Models/WordCompanionPresenceModels.cs b/PlotLine/Models/WordCompanionPresenceModels.cs new file mode 100644 index 0000000..3db10b9 --- /dev/null +++ b/PlotLine/Models/WordCompanionPresenceModels.cs @@ -0,0 +1,44 @@ +namespace PlotLine.Models; + +public static class WordCompanionPresenceStatuses +{ + public const string Connecting = "Connecting"; + public const string Connected = "Connected"; + public const string Offline = "Offline"; +} + +public sealed class WordCompanionPresenceRegistration +{ + public int? UserID { get; init; } + public string? CompanionVersion { get; init; } + public string? MachineName { get; init; } + public bool DocumentOpen { get; init; } + public string? CurrentDocumentName { get; init; } + public int? LinkedProjectID { get; init; } + public int? LinkedBookID { get; init; } +} + +public sealed class WordCompanionPresenceHeartbeat +{ + public string? CompanionVersion { get; init; } + public string? MachineName { get; init; } + public bool DocumentOpen { get; init; } + public string? CurrentDocumentName { get; init; } + public int? LinkedProjectID { get; init; } + public int? LinkedBookID { get; init; } +} + +public sealed class WordCompanionPresenceStatus +{ + public int UserID { get; init; } + public bool IsConnected { get; init; } + public string Status { get; init; } = WordCompanionPresenceStatuses.Offline; + public string? CompanionVersion { get; init; } + public string? MachineName { get; init; } + public bool DocumentOpen { get; init; } + public string? CurrentDocumentName { get; init; } + public int? LinkedProjectID { get; init; } + public int? LinkedBookID { get; init; } + public DateTime? ConnectedUtc { get; init; } + public DateTime? LastHeartbeatUtc { get; init; } +} diff --git a/PlotLine/Program.cs b/PlotLine/Program.cs index 9cb1cca..91f2d6d 100644 --- a/PlotLine/Program.cs +++ b/PlotLine/Program.cs @@ -170,6 +170,8 @@ public class Program builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); + builder.Services.AddSingleton(); + builder.Services.AddHostedService(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); diff --git a/PlotLine/Services/OnboardingService.cs b/PlotLine/Services/OnboardingService.cs index ec6797b..39896e9 100644 --- a/PlotLine/Services/OnboardingService.cs +++ b/PlotLine/Services/OnboardingService.cs @@ -26,6 +26,7 @@ public sealed class OnboardingService( IBookRepository books, IBookService bookService, IProjectActivityService activity, + IWordCompanionPresenceService companionPresence, ICurrentUserService currentUser) : IOnboardingService { private static readonly HashSet WritingJourneys = new(StringComparer.Ordinal) @@ -62,6 +63,7 @@ public sealed class OnboardingService( var selectedBook = state.BookID.HasValue ? await books.GetAsync(state.BookID.Value) : null; + var presence = await companionPresence.GetStatusAsync(RequireUserId()); return new OnboardingWizardViewModel { @@ -73,6 +75,7 @@ public sealed class OnboardingService( BookID = state.BookID, SelectedProjectName = selectedProject?.ProjectName, SelectedBookTitle = selectedBook is null ? null : BookOptionTitle(selectedBook), + CompanionPresence = ToPresenceViewModel(presence), ProjectOptions = projectOptions, BookOptions = bookOptions, ProjectForm = new ProjectOnboardingForm @@ -113,6 +116,22 @@ public sealed class OnboardingService( } var state = await onboarding.GetAsync(currentUser.UserId.Value); + if (state is not null && IsMicrosoftWordState(state)) + { + var status = await companionPresence.GetStatusAsync(currentUser.UserId.Value); + return new OnboardingNudgeViewModel + { + ShouldShow = true, + IsCompanionPresence = true, + CompanionConnected = status.IsConnected, + Title = status.IsConnected ? "Word Companion Connected" : "Connect your Word Companion", + Description = status.IsConnected + ? "Your Word Companion is ready for this story setup." + : "Open Word, start the Companion, and sign in with your PlotDirector account.", + ButtonText = status.IsConnected ? "View setup" : "Connect Companion" + }; + } + if (state?.OnboardingCompletedUtc is not null) { return new OnboardingNudgeViewModel { ShouldShow = false }; @@ -137,8 +156,15 @@ public sealed class OnboardingService( } public async Task ShouldShowWordCompanionHeaderAsync() - => currentUser.UserId.HasValue - && await onboarding.ShouldShowWordCompanionHeaderAsync(currentUser.UserId.Value); + { + if (!currentUser.UserId.HasValue) + { + return false; + } + + var state = await onboarding.GetAsync(currentUser.UserId.Value); + return state is not null && IsMicrosoftWordState(state); + } public async Task MoveToNextStepAsync() { @@ -391,6 +417,20 @@ public sealed class OnboardingService( private static string Clean(string? value) => (value ?? string.Empty).Trim(); + private static bool IsMicrosoftWordState(UserOnboardingState state) + => string.Equals(state.WritingSoftware, WritingSoftwareValues.MicrosoftWord, StringComparison.Ordinal) + || state.UsesWordCompanion == true; + + private static CompanionPresenceViewModel ToPresenceViewModel(WordCompanionPresenceStatus status) + => new() + { + IsConnected = status.IsConnected, + Status = status.Status, + CompanionVersion = status.CompanionVersion, + CurrentDocumentName = status.CurrentDocumentName, + LastHeartbeatUtc = status.LastHeartbeatUtc + }; + private static string? CleanOptional(string? value) { var cleaned = Clean(value); diff --git a/PlotLine/Services/WordCompanionPresenceMonitor.cs b/PlotLine/Services/WordCompanionPresenceMonitor.cs new file mode 100644 index 0000000..31f2345 --- /dev/null +++ b/PlotLine/Services/WordCompanionPresenceMonitor.cs @@ -0,0 +1,32 @@ +using Microsoft.AspNetCore.SignalR; +using PlotLine.Hubs; + +namespace PlotLine.Services; + +public sealed class WordCompanionPresenceMonitor( + IWordCompanionPresenceService presence, + IHubContext hub) : BackgroundService +{ + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + using var timer = new PeriodicTimer(TimeSpan.FromSeconds(10)); + try + { + while (await timer.WaitForNextTickAsync(stoppingToken)) + { + var offlineStatuses = await presence.MarkStaleOfflineAsync(); + foreach (var status in offlineStatuses) + { + await hub.Clients + .Group(PresenceGroup(status.UserID)) + .SendAsync("WordCompanionPresenceChanged", status, stoppingToken); + } + } + } + catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested) + { + } + } + + private static string PresenceGroup(int userId) => $"word-companion-presence:{userId}"; +} diff --git a/PlotLine/Services/WordCompanionPresenceService.cs b/PlotLine/Services/WordCompanionPresenceService.cs new file mode 100644 index 0000000..2115772 --- /dev/null +++ b/PlotLine/Services/WordCompanionPresenceService.cs @@ -0,0 +1,173 @@ +using System.Collections.Concurrent; +using PlotLine.Models; + +namespace PlotLine.Services; + +public interface IWordCompanionPresenceService +{ + Task RegisterAsync(int userId, string connectionId, WordCompanionPresenceRegistration registration); + Task HeartbeatAsync(int userId, string connectionId, WordCompanionPresenceHeartbeat heartbeat); + Task DisconnectAsync(string connectionId); + Task GetStatusAsync(int userId); + Task IsConnectedAsync(int userId); + Task> MarkStaleOfflineAsync(); +} + +public sealed class WordCompanionPresenceService : IWordCompanionPresenceService +{ + private static readonly TimeSpan HeartbeatTimeout = TimeSpan.FromSeconds(55); + private readonly ConcurrentDictionary recordsByUser = new(); + private readonly ConcurrentDictionary usersByConnection = new(StringComparer.Ordinal); + + public Task RegisterAsync(int userId, string connectionId, WordCompanionPresenceRegistration registration) + { + var now = DateTime.UtcNow; + var record = new PresenceRecord + { + UserID = userId, + ConnectionID = connectionId, + Status = WordCompanionPresenceStatuses.Connected, + CompanionVersion = Clean(registration.CompanionVersion), + MachineName = Clean(registration.MachineName), + DocumentOpen = registration.DocumentOpen, + CurrentDocumentName = Clean(registration.CurrentDocumentName), + LinkedProjectID = Positive(registration.LinkedProjectID), + LinkedBookID = Positive(registration.LinkedBookID), + ConnectedUtc = now, + LastHeartbeatUtc = now + }; + + recordsByUser.AddOrUpdate(userId, record, (_, _) => record); + usersByConnection[connectionId] = userId; + return Task.FromResult(ToStatus(record, now)); + } + + public Task HeartbeatAsync(int userId, string connectionId, WordCompanionPresenceHeartbeat heartbeat) + { + var now = DateTime.UtcNow; + var record = recordsByUser.AddOrUpdate( + userId, + _ => new PresenceRecord + { + UserID = userId, + ConnectionID = connectionId, + ConnectedUtc = now + }, + (_, existing) => + { + existing.ConnectionID = connectionId; + return existing; + }); + + record.Status = WordCompanionPresenceStatuses.Connected; + record.CompanionVersion = Clean(heartbeat.CompanionVersion) ?? record.CompanionVersion; + record.MachineName = Clean(heartbeat.MachineName) ?? record.MachineName; + record.DocumentOpen = heartbeat.DocumentOpen; + record.CurrentDocumentName = Clean(heartbeat.CurrentDocumentName); + record.LinkedProjectID = Positive(heartbeat.LinkedProjectID); + record.LinkedBookID = Positive(heartbeat.LinkedBookID); + record.LastHeartbeatUtc = now; + + usersByConnection[connectionId] = userId; + return Task.FromResult(ToStatus(record, now)); + } + + public Task DisconnectAsync(string connectionId) + { + if (!usersByConnection.TryRemove(connectionId, out var userId) + || !recordsByUser.TryGetValue(userId, out var record) + || !string.Equals(record.ConnectionID, connectionId, StringComparison.Ordinal)) + { + return Task.FromResult(null); + } + + record.Status = WordCompanionPresenceStatuses.Offline; + record.ConnectionID = string.Empty; + return Task.FromResult(ToStatus(record, DateTime.UtcNow)); + } + + public Task GetStatusAsync(int userId) + { + if (!recordsByUser.TryGetValue(userId, out var record)) + { + return Task.FromResult(new WordCompanionPresenceStatus { UserID = userId }); + } + + return Task.FromResult(ToStatus(record, DateTime.UtcNow)); + } + + public async Task IsConnectedAsync(int userId) + => (await GetStatusAsync(userId)).IsConnected; + + public Task> MarkStaleOfflineAsync() + { + var now = DateTime.UtcNow; + var changed = new List(); + foreach (var record in recordsByUser.Values) + { + if (!string.Equals(record.Status, WordCompanionPresenceStatuses.Connected, StringComparison.Ordinal) + || !record.LastHeartbeatUtc.HasValue + || now - record.LastHeartbeatUtc.Value <= HeartbeatTimeout) + { + continue; + } + + record.Status = WordCompanionPresenceStatuses.Offline; + if (!string.IsNullOrWhiteSpace(record.ConnectionID)) + { + usersByConnection.TryRemove(record.ConnectionID, out _); + record.ConnectionID = string.Empty; + } + + changed.Add(ToStatus(record, now)); + } + + return Task.FromResult>(changed); + } + + private static WordCompanionPresenceStatus ToStatus(PresenceRecord record, DateTime now) + { + var connected = string.Equals(record.Status, WordCompanionPresenceStatuses.Connected, StringComparison.Ordinal) + && record.LastHeartbeatUtc.HasValue + && now - record.LastHeartbeatUtc.Value <= HeartbeatTimeout; + + return new WordCompanionPresenceStatus + { + UserID = record.UserID, + IsConnected = connected, + Status = connected ? WordCompanionPresenceStatuses.Connected : WordCompanionPresenceStatuses.Offline, + CompanionVersion = record.CompanionVersion, + MachineName = record.MachineName, + DocumentOpen = record.DocumentOpen, + CurrentDocumentName = record.CurrentDocumentName, + LinkedProjectID = record.LinkedProjectID, + LinkedBookID = record.LinkedBookID, + ConnectedUtc = record.ConnectedUtc, + LastHeartbeatUtc = record.LastHeartbeatUtc + }; + } + + private static string? Clean(string? value) + { + var cleaned = (value ?? string.Empty).Trim(); + return string.IsNullOrWhiteSpace(cleaned) ? null : cleaned; + } + + private static int? Positive(int? value) + => value is > 0 ? value.Value : null; + + private sealed class PresenceRecord + { + public int UserID { get; init; } + public string ConnectionID { get; set; } = string.Empty; + public string Status { get; set; } = WordCompanionPresenceStatuses.Offline; + public string? CompanionVersion { get; set; } + public string? MachineName { get; set; } + public bool DocumentOpen { get; set; } + public string? CurrentDocumentName { get; set; } + public int? LinkedProjectID { get; set; } + public int? LinkedBookID { get; set; } + public DateTime? ConnectedUtc { get; set; } + public DateTime? LastHeartbeatUtc { get; set; } + } +} diff --git a/PlotLine/ViewModels/OnboardingViewModels.cs b/PlotLine/ViewModels/OnboardingViewModels.cs index 42d3a80..1bf234d 100644 --- a/PlotLine/ViewModels/OnboardingViewModels.cs +++ b/PlotLine/ViewModels/OnboardingViewModels.cs @@ -15,6 +15,7 @@ public sealed class OnboardingWizardViewModel public string? SelectedProjectName { get; set; } public string? SelectedBookTitle { get; set; } public bool IsMicrosoftWordPath => string.Equals(WritingSoftware, WritingSoftwareValues.MicrosoftWord, StringComparison.Ordinal); + public CompanionPresenceViewModel CompanionPresence { get; set; } = new(); public IReadOnlyList Options { get; set; } = []; public IReadOnlyList ProjectOptions { get; set; } = []; public IReadOnlyList BookOptions { get; set; } = []; @@ -98,4 +99,15 @@ public sealed class OnboardingNudgeViewModel public string ButtonText { get; init; } = "Start setup"; public string Title { get; init; } = "Set up PlotDirector around the way you write"; public string Description { get; init; } = "Answer a few quick questions so PlotDirector can guide your first project."; + public bool IsCompanionPresence { get; init; } + public bool CompanionConnected { get; init; } +} + +public sealed class CompanionPresenceViewModel +{ + public bool IsConnected { get; init; } + public string Status { get; init; } = "Offline"; + public string? CompanionVersion { get; init; } + public string? CurrentDocumentName { get; init; } + public DateTime? LastHeartbeatUtc { get; init; } } diff --git a/PlotLine/ViewModels/WordCompanionViewModels.cs b/PlotLine/ViewModels/WordCompanionViewModels.cs index d1f3d25..f9ec328 100644 --- a/PlotLine/ViewModels/WordCompanionViewModels.cs +++ b/PlotLine/ViewModels/WordCompanionViewModels.cs @@ -4,6 +4,7 @@ public sealed class WordCompanionHostViewModel { public bool IsAuthenticated { get; init; } public bool IsAdmin { get; init; } + public int? UserID { get; init; } public string? DisplayName { get; init; } public string LoginUrl { get; init; } = string.Empty; } diff --git a/PlotLine/Views/Onboarding/Index.cshtml b/PlotLine/Views/Onboarding/Index.cshtml index 489d694..74c2f2e 100644 --- a/PlotLine/Views/Onboarding/Index.cshtml +++ b/PlotLine/Views/Onboarding/Index.cshtml @@ -236,8 +236,8 @@

Ready for the next phase

@if (Model.IsMicrosoftWordPath) { -

Next: connect the Word Companion

-

In the next phase, PlotDirector will guide you through opening the Word Companion and linking it to @Model.SelectedBookTitle.

+

Connect your Word Companion

+

Open Microsoft Word, open your manuscript, start the PlotDirector Word Companion, and sign in using your PlotDirector account.

} else { @@ -245,6 +245,36 @@

In a later phase, PlotDirector will let you upload a .docx export from your writing tool and connect it to @Model.SelectedBookTitle.

} + @if (Model.IsMicrosoftWordPath) + { +
+ +
+

Word Companion

+

@(Model.CompanionPresence.IsConnected ? "Word Companion Connected" : "Waiting for Word Companion...")

+

+ @(Model.CompanionPresence.IsConnected + ? "Your Companion is connected. Continue when you are ready." + : "Once connected this page will update automatically. You stay in control before continuing.") +

+
+
+
Version
+
@(Model.CompanionPresence.CompanionVersion ?? "Waiting")
+
+
+
Document
+
@(Model.CompanionPresence.CurrentDocumentName ?? "No document reported yet")
+
+
+
+
+ }
Writing journey
@@ -272,7 +302,7 @@ Go to Project Overview }
- +
} diff --git a/PlotLine/Views/Projects/Index.cshtml b/PlotLine/Views/Projects/Index.cshtml index f2ccaf7..c079088 100644 --- a/PlotLine/Views/Projects/Index.cshtml +++ b/PlotLine/Views/Projects/Index.cshtml @@ -35,11 +35,13 @@ @if (Model.OnboardingNudge.ShouldShow) { -
+

First setup

-

Set up PlotDirector around the way you write

-

Answer a few quick questions so PlotDirector can guide your first project.

+

@Model.OnboardingNudge.Title

+

@Model.OnboardingNudge.Description

@Model.OnboardingNudge.ButtonText
diff --git a/PlotLine/Views/Shared/_Layout.cshtml b/PlotLine/Views/Shared/_Layout.cshtml index cb24d16..405155b 100644 --- a/PlotLine/Views/Shared/_Layout.cshtml +++ b/PlotLine/Views/Shared/_Layout.cshtml @@ -158,6 +158,13 @@
@if (showWordCompanionHeader) { + + + Offline + @@ -312,6 +319,10 @@ + @if (User.Identity?.IsAuthenticated == true) + { + + } @await RenderSectionAsync("Scripts", required: false) diff --git a/PlotLine/Views/WordCompanionHost/Index.cshtml b/PlotLine/Views/WordCompanionHost/Index.cshtml index 3f7aaad..1efbef1 100644 --- a/PlotLine/Views/WordCompanionHost/Index.cshtml +++ b/PlotLine/Views/WordCompanionHost/Index.cshtml @@ -13,7 +13,11 @@ -
+

PlotDirector

@@ -546,6 +550,10 @@ }
+ @if (Model.IsAuthenticated) + { + + } diff --git a/PlotLine/wwwroot/css/onboarding.css b/PlotLine/wwwroot/css/onboarding.css index 7f636e9..b7b601b 100644 --- a/PlotLine/wwwroot/css/onboarding.css +++ b/PlotLine/wwwroot/css/onboarding.css @@ -151,6 +151,98 @@ margin: 0; } +.onboarding-companion-card { + display: grid; + grid-template-columns: auto 1fr; + gap: 1.1rem; + align-items: center; + margin: 1rem 0 1.25rem; + padding: 1.1rem; + border: 1px solid rgba(31, 42, 68, .12); + border-radius: 8px; + background: linear-gradient(135deg, rgba(47, 111, 99, .1), rgba(182, 138, 70, .12)); +} + +.onboarding-companion-mark { + position: relative; + width: 4.25rem; + aspect-ratio: 1; + border-radius: 50%; + background: rgba(47, 111, 99, .12); + display: grid; + place-items: center; +} + +.onboarding-companion-mark::before, +.onboarding-companion-mark::after { + content: ""; + position: absolute; + inset: .55rem; + border: 2px solid rgba(47, 111, 99, .28); + border-radius: 50%; + animation: onboardingPulse 1.8s ease-out infinite; +} + +.onboarding-companion-mark::after { + animation-delay: .6s; +} + +.onboarding-companion-mark span { + width: 1.25rem; + aspect-ratio: 1; + border-radius: 50%; + background: #b68a46; + box-shadow: 0 0 0 .45rem rgba(182, 138, 70, .18); +} + +.onboarding-companion-card.is-connected .onboarding-companion-mark span, +.word-companion-presence-pill.is-connected .word-companion-presence-dot { + background: #2f6f63; +} + +.onboarding-companion-card.is-connected .onboarding-companion-mark::before, +.onboarding-companion-card.is-connected .onboarding-companion-mark::after { + animation-play-state: paused; + border-color: rgba(47, 111, 99, .18); +} + +.onboarding-companion-body h2 { + margin: 0 0 .35rem; + font-size: 1.35rem; +} + +.onboarding-companion-body p:not(.eyebrow) { + margin: 0; + color: var(--bs-secondary-color); +} + +.onboarding-companion-details { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: .75rem; + margin: 1rem 0 0; +} + +.onboarding-companion-details div { + min-width: 0; + border: 1px solid rgba(31, 42, 68, .1); + border-radius: 8px; + padding: .75rem; + background: rgba(255, 255, 255, .56); +} + +.onboarding-companion-details dt { + color: var(--bs-secondary-color); + font-weight: 700; + font-size: .82rem; +} + +.onboarding-companion-details dd { + margin: .15rem 0 0; + font-weight: 700; + overflow-wrap: anywhere; +} + .onboarding-summary div { display: flex; align-items: center; @@ -192,6 +284,47 @@ color: var(--bs-secondary-color); } +.onboarding-dashboard-nudge.is-connected { + border-color: rgba(47, 111, 99, .28); + background: rgba(47, 111, 99, .12); +} + +.word-companion-presence-pill { + display: inline-flex; + align-items: center; + gap: .45rem; + min-height: 31px; + border: 1px solid rgba(31, 42, 68, .16); + border-radius: 999px; + padding: .25rem .7rem; + background: rgba(255, 255, 255, .72); + color: var(--bs-body-color); + font-size: .875rem; + font-weight: 700; +} + +.word-companion-presence-dot { + width: .58rem; + aspect-ratio: 1; + border-radius: 50%; + background: #b68a46; +} + +.word-companion-presence-pill.is-offline .word-companion-presence-dot { + background: #8b949e; +} + +@keyframes onboardingPulse { + 0% { + opacity: .75; + transform: scale(.76); + } + 100% { + opacity: 0; + transform: scale(1.28); + } +} + @media (max-width: 720px) { .onboarding-choice-grid, .onboarding-option-grid--software { @@ -199,8 +332,14 @@ } .onboarding-dashboard-nudge, + .onboarding-companion-card, .onboarding-summary div { align-items: stretch; + grid-template-columns: 1fr; flex-direction: column; } + + .onboarding-companion-details { + grid-template-columns: 1fr; + } } diff --git a/PlotLine/wwwroot/js/word-companion-host.js b/PlotLine/wwwroot/js/word-companion-host.js index 8a98a9d..c252fe1 100644 --- a/PlotLine/wwwroot/js/word-companion-host.js +++ b/PlotLine/wwwroot/js/word-companion-host.js @@ -165,6 +165,8 @@ const diagnosticHeading1 = document.querySelector("[data-diagnostic-heading1]"); const diagnosticHeading2 = document.querySelector("[data-diagnostic-heading2]"); const isAuthenticated = shell?.dataset.authenticated === "true"; + const userId = Number.parseInt(shell?.dataset.userId || "", 10); + const companionVersion = String(shell?.dataset.companionVersion || "20C").trim(); let projects = []; let books = []; let officeReady = false; @@ -242,6 +244,8 @@ let lastLinkedSceneTitle = ""; let lastLinkedChapterTitle = ""; let runtimeLastDebugAt = 0; + let companionPresenceConnection = null; + let companionPresenceHeartbeatTimer = null; let currentSceneLinks = { characters: [], assets: [], @@ -1182,6 +1186,93 @@ return books.find((book) => book.bookId === bookId) || null; }; + const selectedPresenceProjectId = () => { + const projectId = firstRunDocumentBinding?.projectId + || selectedFirstRunProject()?.projectId + || selectedProject()?.projectId + || readStoredId(storageKeys.projectId); + return Number.isInteger(projectId) && projectId > 0 ? projectId : null; + }; + + const selectedPresenceBookId = () => { + const bookId = firstRunDocumentBinding?.bookId + || selectedFirstRunBook()?.bookId + || selectedBook()?.bookId + || readStoredId(storageKeys.bookId); + return Number.isInteger(bookId) && bookId > 0 ? bookId : null; + }; + + const currentDocumentName = () => { + const url = String(window.Office?.context?.document?.url || "").trim(); + if (!url) { + return ""; + } + + const lastSegment = url.split(/[\\/]/).filter(Boolean).pop() || ""; + try { + return decodeURIComponent(lastSegment); + } catch { + return lastSegment; + } + }; + + const companionPresencePayload = () => ({ + userID: Number.isInteger(userId) && userId > 0 ? userId : null, + companionVersion, + machineName: "", + documentOpen: !!wordHostAvailable, + currentDocumentName: currentDocumentName(), + linkedProjectID: selectedPresenceProjectId(), + linkedBookID: selectedPresenceBookId() + }); + + const sendCompanionHeartbeat = async () => { + if (!companionPresenceConnection || companionPresenceConnection.state !== signalR.HubConnectionState.Connected) { + return; + } + + await companionPresenceConnection.invoke("CompanionHeartbeat", companionPresencePayload()); + }; + + const refreshCompanionPresence = () => { + sendCompanionHeartbeat().catch((error) => { + console.debug("Word Companion heartbeat unavailable.", error); + }); + }; + + const startCompanionPresence = async () => { + if (!isAuthenticated || !window.signalR || companionPresenceConnection) { + return; + } + + companionPresenceConnection = new signalR.HubConnectionBuilder() + .withUrl("/hubs/word-companion-follow") + .withAutomaticReconnect() + .build(); + + companionPresenceConnection.onreconnected(refreshCompanionPresence); + companionPresenceConnection.onclose(() => { + if (companionPresenceHeartbeatTimer) { + window.clearInterval(companionPresenceHeartbeatTimer); + companionPresenceHeartbeatTimer = null; + } + }); + + await companionPresenceConnection.start(); + await companionPresenceConnection.invoke("RegisterCompanion", companionPresencePayload()); + companionPresenceHeartbeatTimer = window.setInterval(refreshCompanionPresence, 25000); + }; + + window.addEventListener("beforeunload", () => { + if (companionPresenceHeartbeatTimer) { + window.clearInterval(companionPresenceHeartbeatTimer); + companionPresenceHeartbeatTimer = null; + } + if (companionPresenceConnection) { + companionPresenceConnection.stop().catch(() => {}); + } + }); + const setFirstRunStatus = (message) => setText(firstRunStatus, message); const updateFirstRunButtons = () => { @@ -5177,6 +5268,7 @@ resetStructurePreview(); resetFirstRunPreview("Select a Book to begin."); await loadBooks(projectId, null); + refreshCompanionPresence(); }); bookSelect?.addEventListener("change", () => { @@ -5194,6 +5286,7 @@ resetFirstRunPreview(selectedFirstRunBook() ? "Ready to scan manuscript structure." : "Select a Book to begin."); + refreshCompanionPresence(); }); firstRunProjectSelect?.addEventListener("change", async () => { @@ -5206,6 +5299,7 @@ writeStoredId(storageKeys.bookId, null); resetFirstRunPreview("Select a Book to begin."); await loadBooks(projectId, null); + refreshCompanionPresence(); }); firstRunBookSelect?.addEventListener("change", () => { @@ -5219,6 +5313,7 @@ resetFirstRunPreview(selectedFirstRunBook() ? "Ready to scan manuscript structure." : "Select a Book to begin."); + refreshCompanionPresence(); }); firstRunNewBookTitle?.addEventListener("input", updateFirstRunButtons); @@ -5320,6 +5415,9 @@ ready: "No", wordApi: "No" }); + startCompanionPresence().catch((error) => { + console.debug("Word Companion presence unavailable.", error); + }); return; } @@ -5328,6 +5426,7 @@ setOfficeStatus("Word Companion ready"); await projectsLoadPromise; await initialiseFirstRunBinding(); + await startCompanionPresence(); if (!wordHostAvailable) { setDocumentMessage("Word document access is unavailable."); setSceneTrackingState("Manual"); diff --git a/PlotLine/wwwroot/js/word-companion-host.min.js b/PlotLine/wwwroot/js/word-companion-host.min.js index 61add68..73a51c3 100644 --- a/PlotLine/wwwroot/js/word-companion-host.min.js +++ b/PlotLine/wwwroot/js/word-companion-host.min.js @@ -1 +1,25 @@ -(()=>{const e="plotdirector.word.projectId",t="plotdirector.word.bookId",n="plotdirector.word.activeTab",r={documentGuid:"plotdirector.word.documentGuid",bookId:"plotdirector.word.boundBookId",projectId:"plotdirector.word.boundProjectId",bindingVersion:"plotdirector.word.bindingVersion"},o=new Set(["scene","review","maintenance","diagnostics"]),a=document.querySelector(".word-companion-shell"),c=document.querySelector(".word-companion-tabs"),i=[...document.querySelectorAll("[data-tab-button]")],s=[...document.querySelectorAll("[data-tab-panel]")],d=document.querySelector("[data-linked-manuscript-card]"),l=document.querySelector("[data-linked-project-title]"),u=document.querySelector("[data-linked-book-title]"),p=document.querySelector("[data-first-run-wizard]"),h=document.querySelector("[data-first-run-project-select]"),m=document.querySelector("[data-first-run-book-select]"),y=document.querySelector("[data-first-run-new-book-title]"),g=document.querySelector("[data-first-run-create-book]"),f=document.querySelector("[data-first-run-scan]"),w=document.querySelector("[data-first-run-cancel]"),I=document.querySelector("[data-first-run-status]"),S=document.querySelector("[data-first-run-summary]"),b=document.querySelector("[data-first-run-character-section]"),C=document.querySelector("[data-first-run-character-status]"),k=document.querySelector("[data-first-run-character-candidates]"),v=document.querySelector("[data-first-run-character-actions]"),x=document.querySelector("[data-first-run-create-characters]"),N=document.querySelector("[data-first-run-skip-characters]"),E=document.querySelector("[data-first-run-continue]"),L=document.querySelector("[data-first-run-import-actions]"),P=document.querySelector("[data-first-run-import]"),q=document.querySelector("[data-first-run-import-cancel]"),D=document.querySelector("[data-first-run-replace-dialog]"),A=document.querySelector("[data-first-run-replace-confirm]"),$=document.querySelector("[data-first-run-replace-cancel]"),T=document.querySelector("[data-unlink-word-document-dialog]"),j=document.querySelector("[data-unlink-word-document-confirm]"),W=document.querySelector("[data-unlink-word-document-cancel]"),R=document.querySelector("[data-office-status]"),U=document.querySelector("[data-connection-status]"),M=document.querySelector("[data-summary-connection]"),O=document.querySelector("[data-summary-project]"),B=document.querySelector("[data-summary-book]"),G=document.querySelector("[data-project-select]"),H=document.querySelector("[data-book-select]"),F=document.querySelector("[data-runtime-book-selectors]"),V=document.querySelector("[data-project-message]"),Y=document.querySelector("[data-book-message]"),J=document.querySelector("[data-refresh-current-scene]"),_=document.querySelector("[data-refresh-document]"),z=document.querySelector("[data-current-chapter]"),K=document.querySelector("[data-current-scene]"),Q=document.querySelector("[data-current-word-count]"),X=document.querySelector("[data-scene-tracking-status]"),Z=document.querySelector("[data-document-message]"),ee=document.querySelector("[data-sync-note]"),te=document.querySelector("[data-document-outline]"),ne=document.querySelector("[data-analyse-manuscript-structure]"),re=document.querySelector("[data-apply-structure-sync]"),oe=document.querySelector("[data-structure-preview-status]"),ae=document.querySelector("[data-structure-preview-results]"),ce=document.querySelector("[data-refresh-plotdirector-scene]"),ie=document.querySelector("[data-plotdirector-scene-status]"),se=document.querySelector("[data-anchor-status]"),de=document.querySelector("[data-anchor-book-id]"),le=document.querySelector("[data-anchor-chapter-id]"),ue=document.querySelector("[data-anchor-scene-id]"),pe=document.querySelector("[data-attach-plotdirector-ids]"),he=document.querySelector("[data-unlink-word-document]"),me=document.querySelector("[data-plotdirector-scene-details]"),ye=document.querySelector("[data-plotdirector-scene-title]"),ge=document.querySelector("[data-plotdirector-revision-status]"),fe=document.querySelector("[data-plotdirector-estimated-words]"),we=document.querySelector("[data-plotdirector-actual-words]"),Ie=document.querySelector("[data-plotdirector-blocked-status]"),Se=document.querySelector("[data-plotdirector-blocked-reason-row]"),be=document.querySelector("[data-plotdirector-blocked-reason]"),Ce=document.querySelector("[data-runtime-last-sync]"),ke=document.querySelector("[data-sync-scene-progress]"),ve=document.querySelector("[data-sync-word-count]"),xe=document.querySelector("[data-sync-plotdirector-count]"),Ne=document.querySelector("[data-sync-last-synced]"),Ee=document.querySelector("[data-sync-status]"),Le=document.querySelector("[data-writing-brief-block]"),Pe=document.querySelector("[data-writing-brief]"),qe=document.querySelector("[data-plotdirector-link-groups]"),De=document.querySelector("[data-character-review-summary]"),Ae=document.querySelector("[data-asset-review-summary]"),$e=document.querySelector("[data-location-review-summary]"),Te=document.querySelector("[data-character-chips]"),je=document.querySelector("[data-asset-chips]"),We=(document.querySelector("[data-location-chips]"),document.querySelector("[data-primary-location-select]")),Re=document.querySelector("[data-save-scene-links]"),Ue=document.querySelector("[data-scene-links-status]"),Me=document.querySelector("[data-links-editing-scene]"),Oe=document.querySelector("[data-chapter-create-link]"),Be=document.querySelector("[data-chapter-create-link-chapter]"),Ge=document.querySelector("[data-create-plotdirector-chapter]"),He=document.querySelector("[data-link-existing-chapter]"),Fe=document.querySelector("[data-chapter-create-link-status]"),Ve=document.querySelector("[data-scene-create-link]"),Ye=document.querySelector("[data-create-link-chapter]"),Je=document.querySelector("[data-create-link-scene]"),_e=document.querySelector("[data-create-plotdirector-scene]"),ze=document.querySelector("[data-link-existing-scene]"),Ke=document.querySelector("[data-create-link-status]"),Qe=document.querySelector("[data-link-existing-dialog]"),Xe=document.querySelector("[data-link-scene-search]"),Ze=document.querySelector("[data-link-scene-options]"),et=document.querySelector("[data-link-selected-scene]"),tt=document.querySelector("[data-link-dialog-cancel]"),nt=document.querySelector("[data-link-dialog-status]"),rt=document.querySelector("[data-create-scene-dialog]"),ot=document.querySelector("[data-create-dialog-scene]"),at=document.querySelector("[data-create-dialog-chapter]"),ct=document.querySelector("[data-create-dialog-confirm]"),it=document.querySelector("[data-create-dialog-cancel]"),st=document.querySelector("[data-link-existing-chapter-dialog]"),dt=document.querySelector("[data-link-chapter-search]"),lt=document.querySelector("[data-link-chapter-options]"),ut=document.querySelector("[data-link-selected-chapter]"),pt=document.querySelector("[data-link-chapter-dialog-cancel]"),ht=document.querySelector("[data-link-chapter-dialog-status]"),mt=document.querySelector("[data-create-chapter-dialog]"),yt=document.querySelector("[data-create-chapter-dialog-chapter]"),gt=document.querySelector("[data-create-chapter-dialog-book]"),ft=document.querySelector("[data-create-chapter-dialog-confirm]"),wt=document.querySelector("[data-create-chapter-dialog-cancel]"),It=document.querySelector("[data-apply-structure-sync-dialog]"),St=document.querySelector("[data-apply-structure-sync-summary]"),bt=document.querySelector("[data-apply-structure-sync-confirm]"),Ct=document.querySelector("[data-apply-structure-sync-cancel]"),kt=document.querySelector("[data-resolve-project-id]"),vt=document.querySelector("[data-resolve-project-title]"),xt=document.querySelector("[data-resolve-book-id]"),Nt=document.querySelector("[data-resolve-book-title]"),Et=document.querySelector("[data-resolve-detected-chapter]"),Lt=document.querySelector("[data-resolve-detected-scene]"),Pt=document.querySelector("[data-resolve-request-chapter]"),qt=document.querySelector("[data-resolve-request-scene]"),Dt=document.querySelector("[data-resolve-result]"),At=document.querySelector("[data-resolve-chapter-id]"),$t=document.querySelector("[data-resolve-scene-id]"),Tt=document.querySelector("[data-run-diagnostics]"),jt=document.querySelector("[data-diagnostic-host]"),Wt=document.querySelector("[data-diagnostic-platform]"),Rt=document.querySelector("[data-diagnostic-ready]"),Ut=document.querySelector("[data-diagnostic-word-api]"),Mt=document.querySelector("[data-diagnostic-last-scan]"),Ot=document.querySelector("[data-diagnostic-last-error]"),Bt=document.querySelector("[data-diagnostic-anchor-type]"),Gt=document.querySelector("[data-diagnostic-stored-scene-id]"),Ht=document.querySelector("[data-diagnostic-stored-chapter-id]"),Ft=document.querySelector("[data-diagnostic-resolution-method]"),Vt=document.querySelector("[data-diagnostic-paragraphs]"),Yt=document.querySelector("[data-diagnostic-heading1]"),Jt=document.querySelector("[data-diagnostic-heading2]"),_t="true"===a?.dataset.authenticated;let zt=[],Kt=[],Qt=!1,Xt=!1,Zt="Unknown",en="Unknown",tn="",nn="",rn=null,on=null,an=null,cn=null,sn=null,dn=null,ln="",un="Title Match",pn=!1,hn=[],mn=null,yn=null,gn=[],fn=null,wn=null,In=null,Sn=null,bn=null,Cn=null,kn=null,vn=null,xn=null,Nn=null,En=[],Ln=!1,Pn=!1,qn=0,Dn=!1,An=!1,$n=!1,Tn=null,jn="Manual",Wn=!1,Rn=null,Un=!1,Mn=0,On=!1,Bn=0,Gn=null;const Hn=1200;let Fn=!1,Vn=!1,Yn=null,Jn=!1,_n=!1,zn=null,Kn=null,Qn=null,Xn=!1,Zn=!1,er=null,tr="",nr=[],rr=null,or="",ar=null,cr="",ir=[],sr=null,dr="",lr=null,ur="",pr=[],hr=null,mr="",yr="",gr="",fr="",wr=0,Ir={characters:[],assets:[],locations:[],primaryLocationId:null};const Sr=e=>{const t=Date.now();t-wr<1e3||(wr=t,console.debug(`[Word Companion Runtime] ${e}`))},br=e=>{console.debug(`[Word Companion Timing] ${e}`)},Cr=e=>{R&&(R.textContent=e)},kr=e=>"links"===e?"review":"structure"===e?"maintenance":e,vr=(e,t=!0)=>{const r=kr(e),a=o.has(r)&&i.some(e=>e.dataset.tabButton===r)?r:"scene";for(const e of i){const t=e.dataset.tabButton===a;e.classList.toggle("is-active",t),e.setAttribute("aria-selected",t?"true":"false")}for(const e of s){const t=e.dataset.tabPanel===a;e.classList.toggle("is-active",t),e.hidden=!t}t&&((e,t)=>{try{t?window.localStorage.setItem(e,String(t)):window.localStorage.removeItem(e)}catch{}})(n,a)},xr=()=>{const e=kr((e=>{try{return window.localStorage.getItem(e)||""}catch{return""}})(n));vr(o.has(e)?e:"scene",!1)},Nr=e=>{U&&(U.textContent=e),M&&(M.textContent=e)},Er=e=>{V&&(V.textContent=e)},Lr=e=>{Y&&(Y.textContent=e)},Pr=e=>{Z&&(Z.textContent=e)},qr=e=>{ie&&(ie.textContent=e)},Dr=e=>{const t=t=>Object.prototype.hasOwnProperty.call(e,t);t("anchorType")&&Co(Bt,zr(e.anchorType)),t("storedSceneId")&&Co(Gt,zr(e.storedSceneId)),t("storedChapterId")&&Co(Ht,zr(e.storedChapterId)),t("resolutionMethod")&&Co(Ft,zr(e.resolutionMethod))},Ar=()=>{const e=$o(),t=Number.isInteger(sn)&&sn>0&&cn===sn,n=!Number.isInteger(dn)||dn<=0||an===dn,r=t&&n;Co(se,r?"Attached to PlotDirector":"Not attached"),Co(de,e?.bookId?String(e.bookId):"-"),Co(le,Number.isInteger(dn)&&dn>0?String(dn):"-"),Co(ue,Number.isInteger(sn)&&sn>0?String(sn):"-"),pe&&(jr(pe,r&&!pn),pe.disabled=!sn||r&&!pn,pe.textContent=cn&&!r?"Reattach PlotDirector IDs":"Attach PlotDirector IDs"),Dr({anchorType:un||"Title Match",storedSceneId:cn,storedChapterId:an,resolutionMethod:ln})},$r=(e,t,n,r=null,o=null,a=null)=>{const c=e||"",i=t||"",s=Number.isInteger(n)?n:null,d=Number.isInteger(a)&&a>0?a:null,l=Number.isInteger(r)&&r>0?r:null,u=Number.isInteger(o)&&o>0?o:null,p=tn===c&&nn===i&&rn===s&&on===d&&an===l&&cn===u;tn=e||"",nn=t||"",rn=Number.isInteger(n)?n:null,on=Number.isInteger(a)&&a>0?a:null,an=Number.isInteger(r)&&r>0?r:null,cn=Number.isInteger(o)&&o>0?o:null,p||(z&&(z.textContent=e||"Not detected"),K&&(K.textContent=t||"Not detected"),Q&&(Q.textContent=Number.isInteger(n)?String(n):"-"),Co(ve,Number.isInteger(n)?String(n):"-"),Ar())},Tr=()=>{Yn&&(window.clearTimeout(Yn),Yn=null)},jr=(e,t)=>{e&&(e.hidden=t)},Wr=()=>{jr(he,!Sn)},Rr=e=>{Co(Ee,e)},Ur=e=>{Co(oe,e)},Mr=(e="Select a project and book to analyse manuscript structure.")=>{if(In=null,ae){ae.innerHTML="";const e=document.createElement("p");e.textContent="No preview generated.",ae.append(e)}Ur(e),Br()},Or=(e=In)=>((e=In)=>da.flatMap(t=>Array.isArray(e?.[t.key])?e[t.key]:[]))(e).filter(e=>"couldLink"===e.category||"wouldCreateChapters"===e.category||"wouldCreateScenes"===e.category),Br=()=>{re&&(re.disabled=$n||0===Or().length)},Gr=()=>{ne&&(ne.disabled=$n||!Ao()||!$o()),Br()},Hr=()=>{Me&&(Me.textContent=sn?`Editing links for: ${gr||nn||"Current scene"}`:"Link a PlotDirector scene first.")},Fr=()=>{const e=Wn||!sn,t=e||!dn||Jn;ke&&(ke.disabled=t),Re&&(Re.disabled=e),We&&(We.disabled=e);for(const t of[Te,je])for(const n of t?[...t.querySelectorAll("input[type='checkbox']")]:[])n.disabled=e},Vr=e=>{"Live"!==e&&"Manual"!==e||(jn=e),Co(X,e||jn||"Manual")},Yr=(e,t="")=>{Wn=!!e,Wn&&(Tr(),rc()),Co(X,Wn?"Stale":jn),Fr(),t&&(Pr(t),eo(t))},Jr=(e,t=null,n="")=>{sn=Number.isInteger(e)&&e>0?e:null,dn=Number.isInteger(t)&&t>0?t:null,ln=n||"",sn!==zn&&(Kn=null),sn||(Tr(),rc()),Fr(),Rr(Wn?"Refresh Current Scene before syncing.":sn?"Ready to sync.":"No resolved PlotDirector scene."),eo(Wn?"Refresh Current Scene before saving.":sn?"Ready to save.":"Link a PlotDirector scene first."),Hr(),Ar()},_r=e=>{J&&(J.disabled=e,J.textContent=e?"Refreshing...":"Refresh Current Scene")},zr=e=>null==e||""===e?"-":String(e),Kr=e=>{if(!e)return"-";const t=e instanceof Date?e:new Date(e);if(Number.isNaN(t.getTime()))return"-";const n=new Date;return`Last synced: ${t.toDateString()===n.toDateString()?"Today":t.toLocaleDateString()} ${t.toLocaleTimeString([],{hour:"2-digit",minute:"2-digit"})}`},Qr=e=>{bn=e||null;const t=bn?.lastSyncUtc||bn?.manuscriptDocument?.lastSyncUtc;Co(Ce,Kr(t)),Co(Ne,Kr(t)),Do()},Xr=()=>{Cn=null,kn=null,vn=null,Bn+=1,Gn=null,oc(),ac(),cc(),Qr(null)},Zr=()=>{kn=null,vn=null},eo=e=>{Co(Ue,e)},to=e=>{Co(Ke,e)},no=e=>{Co(Fe,e)},ro=()=>!!$o()&&!!tn&&!dn,oo=()=>!!$o()&&!!nn&&Number.isInteger(dn)&&dn>0,ao=()=>{jr(Oe,!0),no(""),Ge&&(Ge.disabled=!0,Ge.textContent="Create Chapter in PlotDirector"),He&&(He.disabled=!0,He.textContent="Link to Existing Chapter")},co=(e="")=>{Co(Be,zr(tn)),jr(Oe,!1),no(e);const t=ro();Ge&&(Ge.disabled=!t,Ge.textContent="Create Chapter in PlotDirector"),He&&(He.disabled=!t,He.textContent="Link to Existing Chapter")},io=()=>{jr(Ve,!0),to(""),_e&&(_e.disabled=!0,_e.textContent="Create Scene in PlotDirector"),ze&&(ze.disabled=!0,ze.textContent="Link to Existing Scene")},so=(e="")=>{Co(Ye,zr(tn)),Co(Je,zr(nn)),jr(Ve,!1),to(e);const t=oo();_e&&(_e.disabled=!t,_e.textContent="Create Scene in PlotDirector"),ze&&(ze.disabled=!t,ze.textContent="Link to Existing Scene")},lo=e=>{qr(e),jr(me,!0),jr(Le,!0),jr(qe,!1),ao(),io(),ln="",gr="",fr="",un=cn?"SceneID Anchor":an?"ChapterID Anchor":"Title Match",pn=!1,Ir={characters:[],assets:[],locations:[],primaryLocationId:null},po(Te,[],"character"),po(je,[],"asset"),ho([],null,!1),_o(),Jr(null),Co(xe,"-"),Co(ye,"-"),Co(ge,"-"),Co(fe,"-"),Co(we,"-"),Co(Ie,"-"),Co(be,"-"),jr(Se,!0),Ar(),Hr()},uo=(e,t)=>"asset"===t?e?.assetId||e?.AssetId||e?.storyAssetId||e?.StoryAssetId||0:"location"===t?e?.locationId||e?.LocationId||0:e?.characterId||e?.CharacterId||0,po=(e,t,n,r=!1)=>{if(!e)return;e.innerHTML="";const o=Array.isArray(t)?t:[];if(0===o.length){const t=document.createElement("p");return t.className="word-companion-empty-list",t.textContent="None",void e.append(t)}for(const t of o){const o=Number.parseInt(uo(t,n),10);if(!Number.isInteger(o)||o<=0)continue;const a=document.createElement("label");a.className="word-companion-check-item";const c=document.createElement("input");c.type="checkbox",c.value=String(o),c.checked=!!t.linked,c.disabled=!r;const i=document.createElement("span");i.textContent=t.name||"Unnamed",a.append(c,i),e.append(a)}},ho=(e,t,n=!1)=>{if(!We)return;We.innerHTML="";const r=document.createElement("option");r.value="",r.textContent="None",We.append(r);const o=Number.parseInt(t||"",10);for(const t of Array.isArray(e)?e:[]){const e=Number.parseInt(uo(t,"location"),10);if(!Number.isInteger(e)||e<=0)continue;const n=document.createElement("option");n.value=String(e),n.textContent=t.name||"Unnamed",n.selected=e===o,We.append(n)}We.disabled=!n},mo=e=>String(e||"").trim().replace(/\s+/g," "),yo=(e,t)=>{const n=mo(e);if(!n)return"";const r=new RegExp(`^${"scene"===t?"scene":"chapter"}\\s+(?:\\d+|[ivxlcdm]+|one|two|three|four|five|six|seven|eight|nine|ten|eleven|twelve|thirteen|fourteen|fifteen|sixteen|seventeen|eighteen|nineteen|twenty)\\s*(?::|-|\\u2013|\\u2014)\\s*`,"i");return mo(n.replace(r,""))||n},go=e=>{const t=t=>Object.prototype.hasOwnProperty.call(e,t);t("projectId")&&Co(kt,zr(e.projectId)),t("projectTitle")&&Co(vt,zr(e.projectTitle)),t("bookId")&&Co(xt,zr(e.bookId)),t("bookTitle")&&Co(Nt,zr(e.bookTitle)),t("detectedChapter")&&Co(Et,zr(e.detectedChapter)),t("detectedScene")&&Co(Lt,zr(e.detectedScene)),t("requestChapter")&&Co(Pt,zr(e.requestChapter)),t("requestScene")&&Co(qt,zr(e.requestScene)),t("result")&&Co(Dt,zr(e.result)),t("chapterId")&&Co(At,zr(e.chapterId)),t("sceneId")&&Co($t,zr(e.sceneId))},fo=async e=>{if(vn===e&&Array.isArray(kn?.chapters))return kn.chapters;const t=await ja(`/api/word-companion/books/${e}/structure`);return vn=e,kn=t||null,Array.isArray(t?.chapters)?t.chapters:[]},wo=async e=>{const t=await ja(`/api/word-companion/books/${e}/chapters`);return Array.isArray(t?.chapters)?t.chapters:[]},Io=e=>{if(Number.isInteger(dn)&&dn>0){const t=e.find(e=>e.chapterId===dn);if(t)return t}const t=yo(tn,"chapter").toLocaleLowerCase();return t&&e.find(e=>yo(e.title,"chapter").toLocaleLowerCase()===t)||null},So=async()=>{const e=$o();return e?Io(await fo(e.bookId)):null},bo=async(e,t,n,r,o)=>{const a=await ja(`/api/word-companion/scenes/${t}/companion`);try{a.revisionStatus=await(async(e,t)=>{const n=await ja(`/api/word-companion/books/${e}/structure`),r=Array.isArray(n?.chapters)?n.chapters:[];for(const e of r){const n=(Array.isArray(e.scenes)?e.scenes:[]).find(e=>e.sceneId===t);if(n)return n.revisionStatus||""}return""})(e,t)}catch{a.revisionStatus=""}var c;un=o||"Title Match",pn=!1,Yr(!1),qr("Linked to PlotDirector"),Pr(""),ao(),io(),Jr(t,n,r),c=a,gr=c?.sceneTitle||nn||"",fr=c?.chapterTitle||tn||"",Co(ye,zr(c?.sceneTitle)),Co(ge,zr(c?.revisionStatus||c?.revisionStatusName||"Not provided")),Co(fe,zr(c?.estimatedWords)),Co(we,zr(c?.actualWords)),Co(xe,zr(c?.actualWords)),Co(Ie,c?.blocked?"Blocked":"Not blocked"),c?.blockedReason?(Co(be,c.blockedReason),jr(Se,!1)):(Co(be,"-"),jr(Se,!0)),Pe&&(Pe.textContent=c?.writingBrief||"No writing brief has been added for this scene."),Ir={characters:Array.isArray(c?.characters)?c.characters:[],assets:Array.isArray(c?.assets)?c.assets:[],locations:Array.isArray(c?.locations)?c.locations:[],primaryLocationId:c?.primaryLocationId??null},po(Te,Ir.characters,"character",!!sn&&!Wn),po(je,Ir.assets,"asset",!!sn&&!Wn),ho(Ir.locations,Ir.primaryLocationId,!!sn&&!Wn),_o(),Hr(),eo(Wn?"Refresh Current Scene before saving.":sn?"Ready to save.":"No resolved PlotDirector scene."),jr(me,!1),jr(Le,!1),jr(qe,!1),Ua(t,n),nc(),hc()},Co=(e,t)=>{if(e){const n=null==t?"":String(t);e.textContent!==n&&(e.textContent=n)}},ko=e=>{const t=t=>Object.prototype.hasOwnProperty.call(e,t);t("host")&&Co(jt,e.host),t("platform")&&Co(Wt,e.platform),t("ready")&&Co(Rt,e.ready),t("wordApi")&&Co(Ut,e.wordApi),t("lastScan")&&Co(Mt,e.lastScan),t("lastError")&&Co(Ot,e.lastError),t("paragraphs")&&Co(Vt,e.paragraphs),t("heading1")&&Co(Yt,e.heading1),t("heading2")&&Co(Jt,e.heading2)},vo=e=>{const t=[];return e?.name&&t.push(e.name),e?.code&&e.code!==e.name&&t.push(e.code),e?.message&&t.push(e.message),e?.debugInfo?.errorLocation&&t.push(`Location: ${e.debugInfo.errorLocation}`),t.length>0?t.join(": "):String(e)},xo=async()=>{if(!window.Office||"function"!=typeof window.Office.onReady)return Qt=!1,Xt=!1,Zt="Unavailable",en="Unavailable",ko({host:Zt,platform:en,ready:"No",wordApi:"No"}),null;const e=await window.Office.onReady();return Qt=!0,Zt=e?.host||"Unknown",en=e?.platform||"Unknown",Xt=!!window.Word&&!!window.Office.HostType&&e?.host===window.Office.HostType.Word,ko({host:Zt,platform:en,ready:"Yes",wordApi:Xt?"Yes":"No"}),e},No=e=>{try{const t=window.localStorage.getItem(e),n=Number.parseInt(t||"",10);return Number.isInteger(n)&&n>0?n:null}catch{return null}},Eo=(e,t)=>{try{t?window.localStorage.setItem(e,String(t)):window.localStorage.removeItem(e)}catch{}},Lo=(e,t)=>{if(!e)return;e.innerHTML="";const n=document.createElement("option");n.value="",n.textContent=t,e.append(n),e.disabled=!0},Po=(e,t,n,r,o)=>{if(!e)return;e.innerHTML="";const a=document.createElement("option");a.value="",a.textContent=t,e.append(a);for(const t of n){const n=document.createElement("option");n.value=String(t[r]),n.textContent=t[o],e.append(n)}e.disabled=0===n.length},qo=e=>{const t=String(e?.displayTitle||"").trim();if(t)return t;const n=String(e?.title||"").trim(),r=String(e?.subtitle||"").trim();return r?`${n}: ${r}`:n},Do=()=>{const e=!!Sn;jr(d,!e),jr(F,e),e&&(Co(l,bn?.projectTitle||Ao()?.title||"Project"),Co(u,bn?.bookTitle||qo($o())||"Book"))},Ao=()=>{const e=Number.parseInt(G?.value||"",10);return zt.find(t=>t.projectId===e)||null},$o=()=>{const e=Number.parseInt(H?.value||"",10);return Kt.find(t=>t.bookId===e)||null},To=()=>{const e=Number.parseInt(h?.value||"",10);return zt.find(t=>t.projectId===e)||null},jo=()=>{const e=Number.parseInt(m?.value||"",10);return Kt.find(t=>t.bookId===e)||null},Wo=e=>Co(I,e),Ro=()=>{const e=To(),t=jo(),n=Pn&&Date.now()-qn>=750;f&&(f.disabled=!e||!t||Dn),g&&(g.disabled=!e||!String(y?.value||"").trim()||Dn),P&&(P.disabled=!xn?.canImport||!Nn||Dn),x&&(x.disabled=!n||An||0===Bo().length)},Uo=e=>{jr(p,!e),jr(c,e),Wr(),Do();for(const t of s)jr(t,!!e||"scene"!==t.dataset.tabPanel&&!t.classList.contains("is-active"));e||xr()},Mo=()=>{if(!h)return;if(0===zt.length)return void Lo(h,"No projects available.");Po(h,"Choose a project",zt,"projectId","title");const e=Number.parseInt(G?.value||"",10);Number.isInteger(e)&&e>0&&(h.value=String(e))},Oo=(e="Select a Project and Book to begin.")=>{xn=null,Nn=null,En=[],Ln=!1,Pn=!1,qn=0,jr(S,!0),jr(L,!0),jr(b,!0),jr(v,!0),S&&(S.innerHTML=""),k&&(k.innerHTML=""),Co(C,""),Wo(e),Ro()},Bo=()=>k?[...k.querySelectorAll("input[type='checkbox']:checked")].map(e=>String(e.value||"").trim()).filter(Boolean):[],Go=e=>{if(En=Array.isArray(e)?e:[],b&&k){if(k.innerHTML="",jr(b,!1),jr(v,!Ln||0===En.length),jr(x,!1),jr(N,!1),jr(E,!0),0===En.length)return Co(C,"No likely major characters found."),void Ro();Co(C,`${En.length} likely major characters found.`);for(const e of En){const t=document.createElement("label");t.className="word-companion-character-candidate";const n=document.createElement("input");n.type="checkbox",n.value=e.text||"",n.checked="high"===String(e.confidence||"").trim().toLowerCase(),n.addEventListener("change",Ro);const r=document.createElement("strong");r.textContent=e.text||"";const o=document.createElement("em"),a=String(e.reason||"").trim();o.textContent=a?`${Number(e.mentionCount||0).toLocaleString()} mentions · ${a}`:`${Number(e.mentionCount||0).toLocaleString()} mentions`;const c=document.createElement("span");c.className="word-companion-character-candidate-text",c.append(r,o),t.append(n,c),k.append(t)}Ro()}},Ho=()=>{const e=window.Office?.context?.document?.settings;if(!e)return null;const t=String(e.get(r.documentGuid)||"").trim(),n=Number.parseInt(e.get(r.bookId)||"",10),o=Number.parseInt(e.get(r.projectId)||"",10),a=Number.parseInt(e.get(r.bindingVersion)||"",10);return!t||!Number.isInteger(n)||n<=0||!Number.isInteger(o)||o<=0?null:{documentGuid:t,bookId:n,projectId:o,bindingVersion:Number.isInteger(a)&&a>0?a:1}},Fo=()=>new Promise((e,t)=>{const n=window.Office?.context?.document?.settings;if(n){for(const e of Object.values(r))"function"==typeof n.remove?n.remove(e):n.set(e,"");n.saveAsync(n=>{n.status===window.Office.AsyncResultStatus.Succeeded?e():t(n.error||new Error("Unable to remove Word document metadata."))})}else t(new Error("Word document settings are unavailable."))}),Vo=()=>Sn?.documentGuid?Sn.documentGuid:window.crypto?.randomUUID?window.crypto.randomUUID():"10000000-1000-4000-8000-100000000000".replace(/[018]/g,e=>(Number(e)^16*Math.random()>>Number(e)/4).toString(16)),Yo=()=>Sn?.documentGuid||Ho()?.documentGuid||"",Jo=()=>{const e=Ao(),t=$o();O&&(O.textContent=e?.title||"(Not connected)"),B&&(B.textContent=t?qo(t):"(Not connected)"),ee&&(ee.textContent=t?"":"Select a PlotDirector book before refreshing."),Gr(),Do()},_o=()=>{const e=Array.isArray(Ir.characters)?Ir.characters.length:0,t=Array.isArray(Ir.assets)?Ir.assets.length:0,n=Array.isArray(Ir.locations)?Ir.locations.length:0;Co(De,`Characters (${e})`),Co(Ae,`Assets (${t})`),Co($e,`Locations (${n})`)},zo=e=>String(e?.styleBuiltIn||e?.style||"").replace(/\s+/g,"").toLowerCase(),Ko=(e,t)=>{const n=zo(e);return n===`heading${t}`||n.endsWith(`.heading${t}`)||n.includes(`heading${t}`)},Qo=e=>String(e||"").trim().split(/\s+/).filter(Boolean).length,Xo=(e,t)=>{const n=String(e||"").match(new RegExp(`^${t}-(\\d+)$`,"i"));if(!n)return null;const r=Number.parseInt(n[1],10);return Number.isInteger(r)&&r>0?r:null},Zo=e=>{try{return Array.isArray(e?.contentControls?.items)?e.contentControls.items:[]}catch(e){return console.warn("Word paragraph content controls were not available.",e),[]}},ea=(e,t)=>{const n=Zo(e);for(const e of n){const n=Xo(e.tag,t);if(n)return n}return null},ta=(e,t)=>{const n=[];let r=0,o=0,a=null,c=null;e.forEach((e,t)=>{const i=String(e.text||"").trim();if(i){if(Ko(e,1))return r+=1,a={index:n.length+1,paragraphIndex:t,title:i,anchorId:ea(e,"PD-CHAPTER"),scenes:[]},n.push(a),void(c=null);if(Ko(e,2)){if(o+=1,!a)return;return c={index:a.scenes.length+1,paragraphIndex:t,title:i,anchorId:ea(e,"PD-SCENE"),wordCount:0},void a.scenes.push(c)}c&&(c.wordCount+=Qo(i))}});const i=t.find(e=>String(e.text||"").trim()),s=i?e.findIndex(e=>{return t=e,n=i,String(t?.text||"").trim()===String(n?.text||"").trim()&&zo(t)===zo(n);var t,n}):-1,d=s>=0&&n.filter(e=>e.paragraphIndex<=s).at(-1)||null,l=d&&s>=0&&d.scenes.filter(e=>e.paragraphIndex<=s).at(-1)||null;return{chapters:n,currentChapter:d,currentScene:l,paragraphCount:e.length,heading1Count:r,heading2Count:o}},na=new Set(["***","###"]),ra=(e,t)=>{const n=String(e?.text||"").trim();return{index:t,text:n,styleBuiltIn:e?.styleBuiltIn||"",style:e?.style||"",wordCount:Qo(n),chapterAnchorId:null,sceneAnchorId:null}},oa=(e,t)=>{e&&(e.endParagraphIndex=t)},aa=(e,t)=>{e&&(e.endParagraphIndex=t,oa(e.scenes.at(-1),t))},ca=(e,t)=>{const n=(t||[]).find(e=>String(e.text||"").trim());if(!e||!n)return e?.currentParagraphIndex??-1;const r=e.paragraphs.filter(e=>((e,t)=>String(e?.text||"").trim()===String(t?.text||"").trim()&&zo(e)===zo(t))(e,n)).map(e=>e.index);return 0===r.length?e.currentParagraphIndex??-1:Number.isInteger(e.currentParagraphIndex)?r.reduce((t,n)=>Math.abs(n-e.currentParagraphIndex){if(!Cn)return!1;const t=((e,t)=>!e||!Number.isInteger(t)||t<0?null:e.chapters.filter(e=>e.startParagraphIndex<=t).at(-1)||null)(Cn,e),n=((e,t)=>!e||!Number.isInteger(t)||t<0?null:e.scenes.filter(e=>e.startParagraphIndex<=t).at(-1)||null)(t,e),r=t?.startParagraphIndex!==Cn.currentChapter?.startParagraphIndex||n?.startParagraphIndex!==Cn.currentScene?.startParagraphIndex||t?.anchorId!==Cn.currentChapter?.anchorId||n?.anchorId!==Cn.currentScene?.anchorId;return Cn.currentParagraphIndex=e,Cn.currentChapter=t,Cn.currentScene=n,!!r&&($r(t?.title,n?.title,n?.wordCount,t?.anchorId,n?.anchorId,t?.index),r)},sa=e=>{if(!te)return;if(te.innerHTML="",0===e.chapters.length){const e=document.createElement("p");return e.textContent="No chapters detected. Use Heading 1 for chapter titles.",void te.append(e)}if(!e.chapters.some(e=>e.scenes.length>0)){const e=document.createElement("p");e.textContent="No scenes detected. Use Heading 2 for scene titles.",te.append(e)}for(const t of e.chapters){const e=document.createElement("div");e.className="word-companion-outline-chapter";const n=document.createElement("strong");if(n.textContent=t.title,e.append(n),t.scenes.length>0){const n=document.createElement("ul");for(const e of t.scenes){const t=document.createElement("li");t.textContent=e.title,n.append(t)}e.append(n)}te.append(e)}},da=[{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"}],la={alreadyLinked:"Already Linked",couldLink:"Could Link",wouldCreateChapter:"Would Create Chapter",wouldCreateScene:"Would Create Scene",manualReview:"Manual Review Required"},ua={alreadyLinked:"✓",couldLink:"~",wouldCreateChapter:"+",wouldCreateScene:"+",manualReview:"?"},pa=(e,t)=>yo(e,t).toLocaleLowerCase(),ha=({category:e,action:t,type:n,wordTitle:r,match:o="",anchorStatus:a="None",matches:c=[],pdChapter:i=null,pdScene:s=null,wordChapter:d=null,wordScene:l=null,parentItem:u=null})=>({id:`${n}-${d?.index||0}-${l?.index||0}-${d?.paragraphIndex??l?.paragraphIndex??0}`,category:e,action:t,type:n,wordTitle:r,match:o,anchorStatus:a,matches:c,pdChapter:i,pdScene:s,wordChapter:d,wordScene:l,parentItem:u,result:"Pending",error:""}),ma=(e,t)=>{Array.isArray(e[t.category])&&e[t.category].push(t)},ya=e=>Array.isArray(e?.scenes)?e.scenes:[],ga=(e,t,n)=>{const r=e.anchorId?`PD-CHAPTER-${e.anchorId}`:"None";if(e.anchorId){const o=t.find(t=>t.chapterId===e.anchorId);if(o){const t=ha({category:"alreadyLinked",action:"alreadyLinked",type:"Chapter",wordTitle:e.title,match:`Chapter ${o.chapterId}: ${o.title}`,anchorStatus:r,pdChapter:o,wordChapter:e});return ma(n,t),t}const a=ha({category:"manualReview",action:"manualReview",type:"Chapter",wordTitle:e.title,anchorStatus:`${r} not found`,matches:[],pdChapter:null,wordChapter:e});return ma(n,a),a}const o=t.filter(t=>pa(t.title,"chapter")===pa(e.title,"chapter"));if(1===o.length){const t=ha({category:"couldLink",action:"couldLink",type:"Chapter",wordTitle:e.title,match:`Chapter ${o[0].chapterId}: ${o[0].title}`,anchorStatus:r,pdChapter:o[0],wordChapter:e});return ma(n,t),t}if(o.length>1){const t=ha({category:"manualReview",action:"manualReview",type:"Chapter",wordTitle:e.title,anchorStatus:r,matches:o.map(e=>`Chapter ${e.chapterId}: ${e.title}`),pdChapter:null,wordChapter:e});return ma(n,t),t}const a=ha({category:"wouldCreateChapters",action:"wouldCreateChapter",type:"Chapter",wordTitle:e.title,anchorStatus:r,pdChapter:null,wordChapter:e});return ma(n,a),a},fa=(e,t,n,r)=>{const o=e.anchorId?`PD-SCENE-${e.anchorId}`:"None";if(e.anchorId){const a=((e,t)=>{for(const n of e){const e=ya(n).find(e=>e.sceneId===t);if(e)return{chapter:n,scene:e}}return null})(n,e.anchorId);return a?void ma(r,ha({category:"alreadyLinked",action:"alreadyLinked",type:"Scene",wordTitle:e.title,match:`Scene ${a.scene.sceneId}: ${a.scene.title}`,anchorStatus:o,pdChapter:a.chapter,pdScene:a.scene,wordChapter:t?.wordChapter,wordScene:e,parentItem:t})):void ma(r,ha({category:"manualReview",action:"manualReview",type:"Scene",wordTitle:e.title,anchorStatus:`${o} not found`,wordChapter:t?.wordChapter,wordScene:e,parentItem:t}))}if(!t?.pdChapter&&"manualReview"===t?.category)return void ma(r,ha({category:"manualReview",action:"manualReview",type:"Scene",wordTitle:e.title,anchorStatus:o,matches:["Parent chapter requires manual review."],wordChapter:t?.wordChapter,wordScene:e,parentItem:t}));if(!t?.pdChapter)return void ma(r,ha({category:"wouldCreateScenes",action:"wouldCreateScene",type:"Scene",wordTitle:e.title,anchorStatus:o,wordChapter:t?.wordChapter,wordScene:e,parentItem:t}));const a=ya(t.pdChapter).filter(t=>pa(t.title,"scene")===pa(e.title,"scene"));1!==a.length?a.length>1?ma(r,ha({category:"manualReview",action:"manualReview",type:"Scene",wordTitle:e.title,anchorStatus:o,matches:a.map(e=>`Scene ${e.sceneId}: ${e.title}`),wordChapter:t.wordChapter,wordScene:e,parentItem:t})):ma(r,ha({category:"wouldCreateScenes",action:"wouldCreateScene",type:"Scene",wordTitle:e.title,anchorStatus:o,pdChapter:t.pdChapter,wordChapter:t.wordChapter,wordScene:e,parentItem:t})):ma(r,ha({category:"couldLink",action:"couldLink",type:"Scene",wordTitle:e.title,match:`Scene ${a[0].sceneId}: ${a[0].title}`,anchorStatus:o,pdChapter:t.pdChapter,pdScene:a[0],wordChapter:t.wordChapter,wordScene:e,parentItem:t}))},wa=e=>{const t=document.createElement("article");t.className="word-companion-preview-item";const n=document.createElement("strong");n.textContent=`${ua[e.action]||""} ${e.type}: ${e.wordTitle}`,t.append(n);const r=document.createElement("span");if(r.textContent=`Anchor: ${e.anchorStatus||"None"}`,t.append(r),e.match){const n=document.createElement("span");n.textContent=`Match: ${e.match}`,t.append(n)}if(Array.isArray(e.matches)&&e.matches.length>0){const n=document.createElement("div");n.className="word-companion-preview-matches";const r=document.createElement("span");r.textContent="Matches:",n.append(r);const o=document.createElement("ul");for(const t of e.matches){const e=document.createElement("li");e.textContent=t,o.append(e)}n.append(o),t.append(n)}const o=document.createElement("span");if(o.textContent=`Action: ${la[e.action]||e.action}`,t.append(o),e.result&&"Pending"!==e.result){const n=document.createElement("span");n.textContent=e.error?`Result: ${e.result} - ${e.error}`:`Result: ${e.result}`,t.append(n)}return t},Ia=e=>{if(ae){ae.innerHTML="";for(const t of da){const n=document.createElement("section");n.className="word-companion-preview-group";const r=document.createElement("strong");r.textContent=t.title,n.append(r);const o=Array.isArray(e?.[t.key])?e[t.key]:[];if(0===o.length){const e=document.createElement("p");e.className="word-companion-empty-list",e.textContent="None",n.append(e)}else for(const e of o)n.append(wa(e));ae.append(n)}Br()}},Sa=async e=>{const t=e.document.body.paragraphs,n=e.document.getSelection().paragraphs;t.load("text,styleBuiltIn,style"),n.load("text,styleBuiltIn,style"),await e.sync();try{for(const e of t.items)(Ko(e,1)||Ko(e,2))&&e.contentControls.load("items/tag,title");await e.sync()}catch(e){console.warn("Unable to load Word heading content controls. Continuing without PlotDirector anchors.",e)}const r=ta(t.items,n.items);return r.bodyParagraphs=t.items,r},ba=async(e,t)=>{const n=e.document.body.paragraphs,r=e.document.getSelection().paragraphs;n.load("text,styleBuiltIn,style"),r.load("text,styleBuiltIn,style"),await e.sync();const o=((e,t)=>{const n=e.map(ra),r=[];let o=0,a=0,c=null,i=null,s=0;const d=(e,t=null,n=null)=>c?(oa(i,e.index-1),i={index:c.scenes.length+1,paragraphIndex:e.index,startParagraphIndex:e.index,endParagraphIndex:e.index,title:t||`Scene ${c.scenes.length+1}`,anchorId:n,wordCount:0},c.scenes.push(i),i):null;for(const e of n)e.text&&(Ko(e,1)?(aa(c,e.index-1),o+=1,c={index:r.length+1,paragraphIndex:e.index,startParagraphIndex:e.index,endParagraphIndex:e.index,title:e.text,anchorId:e.chapterAnchorId,wordCount:0,scenes:[]},r.push(c),i=null,d(e,"Scene 1",e.sceneAnchorId),s+=e.wordCount):c&&(t&&na.has(e.text)?(a+=1,d(e)):(c.wordCount+=e.wordCount,i&&(i.wordCount+=e.wordCount),s+=e.wordCount)));return aa(c,n.length-1),{paragraphs:n,chapters:r,usesExplicitScenes:!!t,paragraphCount:n.length,heading1Count:o,heading2Count:a,documentWordCount:s,currentParagraphIndex:null,currentChapter:null,currentScene:null}})(n.items,t),a=ca(o,r.items);return Cn=o,ia(a),Sr("Cache rebuilt."),o},Ca=()=>window.performance&&"function"==typeof window.performance.now?window.performance.now():Date.now(),ka=e=>!!e&&!e.stale&&e.generation===Bn&&e.documentGuid===Yo()&&e.sceneId===sn,va=async(e={})=>{const t=!!e.includeSelection,n=!1!==e.updateDisplay,r=Bn,o=Ca(),a=Ao(),c=$o(),i=Cn?.currentScene;if(!i||!Number.isInteger(i.startParagraphIndex)||!Number.isInteger(i.endParagraphIndex))return null;if(!Qt||!Xt||!window.Word||"function"!=typeof window.Word.run)return null;const s=Ca(),d=await window.Word.run(async e=>{const n=e.document.body.paragraphs;n.load("text");let r=null;return t&&(r=e.document.getSelection().paragraphs,r.load("text,styleBuiltIn,style")),await e.sync(),{bodyTexts:n.items.map(e=>String(e.text||"")),selectionParagraphs:t&&r?r.items.map(e=>({text:String(e.text||""),styleBuiltIn:e.styleBuiltIn,style:e.style})):[]}}),l=Math.round(Ca()-s);if(r!==Bn)return br(`Office snapshot: ${l}ms stale`),{stale:!0,generation:r};let u=!1;if(t){const e=ca(Cn,d.selectionParagraphs);u=ia(e)}const p=Cn?.currentScene;if(!p||!Number.isInteger(p.startParagraphIndex)||!Number.isInteger(p.endParagraphIndex))return null;const h=[];let m=0;const y=Math.min(p.endParagraphIndex,d.bodyTexts.length-1),g=Cn.currentChapter?.startParagraphIndex;for(let e=p.startParagraphIndex;e<=y;e+=1){const t=String(d.bodyTexts[e]||"").trim();t&&!na.has(t)&&e!==g&&(h.push(t),m+=Qo(t))}p.wordCount=m,n&&$r(Cn.currentChapter?.title,p.title,m,Cn.currentChapter?.anchorId,p.anchorId,Cn.currentChapter?.index);const f={documentGuid:Yo(),projectId:a?.projectId||null,bookId:c?.bookId||null,chapterId:dn,sceneId:sn,chapterTitle:Cn.currentChapter?.title||"",sceneTitle:p.title||"",sceneText:h.join("\n"),wordCount:m,selectionSignature:`${Cn.currentParagraphIndex??-1}:${p.startParagraphIndex}:${p.endParagraphIndex}`,selectionChanged:u,generation:r};return Gn=f,br(`Office snapshot: ${l}ms; scene words: ${m}; total snapshot: ${Math.round(Ca()-o)}ms`),f},xa=async()=>ka(Gn)?Gn:await va(),Na=async()=>{if(Pr(""),!Qt||!Xt||!window.Word||"function"!=typeof window.Word.run)return $r(null,null,null),Pr("Word document access is unavailable."),ko({lastScan:"Failure",lastError:"Word document access is unavailable."}),!1;_&&(_.disabled=!0,_.textContent="Scanning..."),Pr("Scanning document structure...");try{const e=await window.Word.run(async e=>{const t=$o();return t?await ba(e,!!t.usesExplicitScenes):(Cn=null,await Sa(e))});return Cn||$r(e.currentChapter?.title,e.currentScene?.title,e.currentScene?.wordCount,e.currentChapter?.anchorId,e.currentScene?.anchorId,e.currentChapter?.index),lo("Not detected"),sa(e),Pr(""),ko({lastScan:"Success",lastError:"-",paragraphs:String(e.paragraphCount),heading1:String(e.heading1Count),heading2:String(e.heading2Count)}),!0}catch(e){const t=vo(e);return console.error("Unable to scan the Word document.",e),$r(null,null,null),Pr(`Unable to scan the Word document: ${t}`),ko({lastScan:"Failure",lastError:t}),!1}finally{_&&(_.disabled=!1,_.textContent="Refresh Document Structure")}},Ea=async()=>{const e=$o();if(!Ao()||!e)return Ur("Select a project and book to analyse manuscript structure."),!1;if(!Qt||!Xt||!window.Word||"function"!=typeof window.Word.run)return Ur("Word document access is unavailable."),ko({lastScan:"Failure",lastError:"Word document access is unavailable."}),!1;ne&&(ne.disabled=!0,ne.textContent="Analysing..."),Ur("Analysing manuscript structure...");try{const t=await window.Word.run(async e=>await Sa(e)),n=await ja(`/api/word-companion/books/${e.bookId}/structure`);return $r(t.currentChapter?.title,t.currentScene?.title,t.currentScene?.wordCount,t.currentChapter?.anchorId,t.currentScene?.anchorId,t.currentChapter?.index),sa(t),In=((e,t)=>{const n={alreadyLinked:[],couldLink:[],wouldCreateChapters:[],wouldCreateScenes:[],manualReview:[]},r=Array.isArray(t?.chapters)?t.chapters:[];for(const t of Array.isArray(e?.chapters)?e.chapters:[]){const e=ga(t,r,n);for(const o of ya(t))fa(o,e,r,n)}return n})(t,n),Ia(In),Ur("Preview generated. No changes have been applied."),ko({lastScan:"Success",lastError:"-",paragraphs:String(t.paragraphCount),heading1:String(t.heading1Count),heading2:String(t.heading2Count)}),!0}catch(e){return console.error("Unable to analyse manuscript structure.",e),Ur("Unable to analyse manuscript structure."),ko({lastScan:"Failure",lastError:vo(e)}),!1}finally{ne&&(ne.textContent="Analyse Manuscript Structure",Gr())}},La=e=>{Tn&&(Tn(e),Tn=null),It?.close?It.close():It&&(It.hidden=!0)},Pa=()=>{const e=((e=In)=>{const t=Or(e);return{linkChapters:t.filter(e=>"couldLink"===e.category&&"Chapter"===e.type).length,linkScenes:t.filter(e=>"couldLink"===e.category&&"Scene"===e.type).length,createChapters:t.filter(e=>"wouldCreateChapters"===e.category).length,createScenes:t.filter(e=>"wouldCreateScenes"===e.category).length,manualReview:Array.isArray(e?.manualReview)?e.manualReview.length:0}})();return(e=>{if(!St)return;St.innerHTML="";const t=[`Link ${e.linkChapters+e.linkScenes} chapters/scenes`,`Create ${e.createChapters} chapters`,`Create ${e.createScenes} scenes`],n=document.createElement("ul");for(const e of t){const t=document.createElement("li");t.textContent=e,n.append(t)}St.append(n)})(e),It?new Promise(e=>{Tn=e,It.showModal?It.showModal():It.hidden=!1}):Promise.resolve(window.confirm(`Apply Structure Sync?\n\nThis will:\n\n- Link ${e.linkChapters+e.linkScenes} chapters/scenes\n- Create ${e.createChapters} chapters\n- Create ${e.createScenes} scenes\n\nManual review items will be skipped.`))},qa=(e,t,n="")=>{e.result=t,e.error=n},Da=e=>Number.isInteger(e?.wordChapter?.index)&&e.wordChapter.index>0?10*e.wordChapter.index:null,Aa=e=>Number.isInteger(e?.wordScene?.index)&&e.wordScene.index>0?10*e.wordScene.index:null,$a=async(e,t)=>{const n=await Wa(`/api/word-companion/books/${e}/chapters`,{title:mo(t.wordTitle),sortOrder:Da(t)});if(!n?.chapterId)throw new Error("Chapter creation did not return a chapter ID.");Zr(),t.pdChapter={chapterId:n.chapterId,title:n.title||t.wordTitle,sortOrder:n.sortOrder||Da(t)||0,scenes:[]},t.match=`Chapter ${t.pdChapter.chapterId}: ${t.pdChapter.title}`},Ta=async(e,t)=>{const n=t.pdChapter||t.parentItem?.pdChapter;if(!n?.chapterId)throw new Error("Scene parent chapter is not available.");const r=await Wa(`/api/word-companion/books/${e}/chapters/${n.chapterId}/scenes`,{sceneTitle:mo(t.wordTitle),sortOrder:Aa(t)});if(!r?.sceneId)throw new Error("Scene creation did not return a scene ID.");Zr(),t.pdChapter=n,t.pdScene={sceneId:r.sceneId,title:t.wordTitle,sortOrder:Aa(t)||0},t.match=`Scene ${t.pdScene.sceneId}: ${t.pdScene.title}`},ja=async(e,t={})=>{const n=await window.fetch(e,{credentials:"same-origin",...t,headers:{Accept:"application/json",...t.headers||{}}});if(!n.ok)throw new Error(`Request failed: ${n.status}`);return await n.json()},Wa=(e,t)=>ja(e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)}),Ra=async(e="")=>{Pn=!1,qn=0,En=[],k&&(k.innerHTML=""),jr(b,!0),Uo(!1),Wo(""),jr(v,!0),e&&Pr(e);const t=await Ma();e&&t&&tn&&Pr(e)},Ua=async(e=sn,t=dn)=>{const n=Ao(),r=$o(),o=Yo(),a=Number.parseInt(e||"",10),c=Number.parseInt(t||"",10);if(!n||!r||!o||!Number.isInteger(a)||a<=0||!Number.isInteger(c)||c<=0)return;const i=`${n.projectId}:${r.bookId}:${a}`;if(yr!==i){yr=i;try{await Wa("/api/word-companion/runtime/current-scene",{documentGuid:o,projectId:n.projectId,bookId:r.bookId,chapterId:c,sceneId:a}),Sr("Current scene follow event sent.")}catch(e){yr="",console.debug("Unable to notify PlotDirector of current scene.",e)}}},Ma=async()=>{const e=$o();if(!(e&&Qt&&Xt&&window.Word&&"function"==typeof window.Word.run))return!1;Pr("Reading manuscript position...");try{Zr(),await Promise.all([ic(!0),sc(!0),dc(!0)]),await fo(e.bookId);const t=await window.Word.run(async t=>await ba(t,!!e.usesExplicitScenes));return sa(t),ko({lastScan:"Success",lastError:"-",paragraphs:String(t.paragraphCount),heading1:String(t.heading1Count),heading2:String(t.heading2Count)}),tn?(nn?await yc():await mc(),Yr(!1),Vr("Live"),Pr(""),!0):(lo("Not detected"),Yr(!1),Vr("Live"),Pr("Manuscript linked. Click inside a chapter to show the current scene."),!0)}catch(e){return console.error("Unable to initialise bound manuscript runtime.",e),Cn=null,Vr("Manual"),Pr(`Unable to read manuscript position: ${vo(e)}`),ko({lastScan:"Failure",lastError:vo(e)}),!1}},Oa=async n=>{Sn=null,wc(),Tr(),Xr(),lo("Not detected"),Vr("Manual"),Nr("Connected"),Eo(e,null),Eo(t,null),G&&(G.value=""),Lo(H,"Select a project first"),Mo(),h&&(h.value=""),Lo(m,"Select a project first"),Oo(n||"Select a Project and Book to begin."),Pr(n||"Manuscript unlinked."),Rr("No resolved PlotDirector scene."),Uo(!0),Wr()},Ba=e=>e?[...e.querySelectorAll("input[type='checkbox']:checked")].map(e=>Number.parseInt(e.value||"",10)).filter(e=>Number.isInteger(e)&&e>0):[],Ga=()=>{const e=Number.parseInt(We?.value||"",10);return Number.isInteger(e)&&e>0?e:null},Ha=(e,t,n,r)=>{const o=((e,t)=>Zo(e).find(e=>Xo(e.tag,t))||null)(e,r),a=o||e.getRange().insertContentControl();return a.title=t,a.tag=n,a.appearance="Hidden",a},Fa=async(e="PlotDirector IDs attached successfully.")=>{if(!sn||!dn)return Pr("No resolved PlotDirector scene."),!1;if((cn&&cn!==sn||an&&an!==dn)&&!window.confirm("This scene is already linked. Replace the existing PlotDirector link?"))return!1;if(!Qt||!Xt||!window.Word||"function"!=typeof window.Word.run)return Pr("Word document access is unavailable."),!1;pe&&(pe.disabled=!0,pe.textContent="Attaching...");try{return await window.Word.run(async e=>{const t=await Sa(e);if(!t.currentChapter||!t.currentScene)throw new Error("No scene detected in Word. Use Heading 2 for scenes.");const n=t.bodyParagraphs[t.currentChapter.paragraphIndex],r=t.bodyParagraphs[t.currentScene.paragraphIndex];if(!n||!r)throw new Error("Unable to locate the current Word headings.");Ha(n,"PlotDirector Chapter",`PD-CHAPTER-${dn}`,"PD-CHAPTER"),Ha(r,"PlotDirector Scene",`PD-SCENE-${sn}`,"PD-SCENE"),await e.sync()}),an=dn,cn=sn,pn=!1,un="SceneID Anchor",ln="Anchor",Pr(e),ko({lastError:"-"}),Ar(),!0}catch(e){return console.error("Unable to attach PlotDirector IDs.",e),Pr("Unable to attach PlotDirector IDs."),ko({lastError:vo(e)}),Ar(),!1}finally{pe&&(pe.textContent=cn===sn?"Attach PlotDirector IDs":"Reattach PlotDirector IDs",Ar())}},Va=async e=>{if(!dn)return no("No resolved PlotDirector chapter."),!1;if(!Qt||!Xt||!window.Word||"function"!=typeof window.Word.run)return no("Word document access is unavailable."),!1;try{return await window.Word.run(async e=>{const t=await Sa(e);if(!t.currentChapter)throw new Error("No chapter detected in Word. Use Heading 1 for chapters.");const n=t.bodyParagraphs[t.currentChapter.paragraphIndex];if(!n)throw new Error("Unable to locate the current Word chapter heading.");Ha(n,"PlotDirector Chapter",`PD-CHAPTER-${dn}`,"PD-CHAPTER"),await e.sync()}),an=dn,pn=!1,un="ChapterID Anchor",ln="Chapter anchor",no(e),Pr(e),ko({lastError:"-"}),Ar(),!0}catch(e){return console.error("Unable to attach PlotDirector chapter ID.",e),no("Unable to attach PlotDirector chapter ID."),ko({lastError:vo(e)}),Ar(),!1}},Ya=async e=>{ao(),await yc(),Pr(e),no(e)},Ja=e=>{wn&&(wn(e),wn=null),mt?.close?mt.close():mt&&(mt.hidden=!0)},_a=()=>{if(!lt)return;const e=mo(dt?.value||"").toLocaleLowerCase(),t=gn.filter(t=>!e||String(t.title||"").toLocaleLowerCase().includes(e));if(lt.innerHTML="",fn=null,ut&&(ut.disabled=!0),0===t.length){const e=document.createElement("p");return e.className="word-companion-empty-list",e.textContent="No chapters found.",void lt.append(e)}for(const e of t){const t=Number.parseInt(e.chapterId||"",10);if(!Number.isInteger(t)||t<=0)continue;const n=document.createElement("label");n.className="word-companion-scene-option";const r=document.createElement("input");r.type="radio",r.name="word-companion-link-chapter",r.value=String(t),r.addEventListener("change",()=>{fn=t,ut&&(ut.disabled=!1)});const o=document.createElement("span");o.textContent=e.title||"Untitled chapter",n.append(r,o),lt.append(n)}},za=()=>{st?.close?st.close():st&&(st.hidden=!0)},Ka=async(e,t,n)=>{const r=$o();if(!r)return to("Select a PlotDirector book first."),!1;await bo(r.bookId,e,t,"Manual link","Title Match");return!!await Fa(n)&&(to(n),io(),!0)},Qa=e=>{yn&&(yn(e),yn=null),rt?.close?rt.close():rt&&(rt.hidden=!0)},Xa=()=>{if(!Ze)return;const e=mo(Xe?.value||"").toLocaleLowerCase(),t=hn.filter(t=>!e||String(t.title||"").toLocaleLowerCase().includes(e));if(Ze.innerHTML="",mn=null,et&&(et.disabled=!0),0===t.length){const e=document.createElement("p");return e.className="word-companion-empty-list",e.textContent="No scenes found.",void Ze.append(e)}for(const e of t){const t=Number.parseInt(e.sceneId||"",10);if(!Number.isInteger(t)||t<=0)continue;const n=document.createElement("label");n.className="word-companion-scene-option";const r=document.createElement("input");r.type="radio",r.name="word-companion-link-scene",r.value=String(t),r.addEventListener("change",()=>{mn=t,et&&(et.disabled=!1)});const o=document.createElement("span");o.textContent=e.title||"Untitled scene",n.append(r,o),Ze.append(n)}},Za=()=>{Qe?.close?Qe.close():Qe&&(Qe.hidden=!0)},ec=()=>{Jn=!1,Fr(),_n&&(_n=!1,nc(1e3))},tc=async(e={})=>{const t=e.source||"manual",n=Ca();if(!sn)return void Rr("No resolved PlotDirector scene.");if(!dn)return void Rr("No resolved PlotDirector chapter.");if(Wn)return void Rr("Refresh Current Scene before syncing.");const r=$o(),o=Yo();if(!r||!o)return void Rr("Bound manuscript metadata is unavailable.");if(Jn)return _n=!0,void Sr("Scene sync deferred; sync already running.");Tr(),Jn=!0,_n=!1;let a=e.snapshot||null;try{a=ka(a)?a:await xa()}catch(e){return Rr("Sync failed"),ko({lastError:vo(e)}),Sr("Scene sync failed."),void ec()}if(!ka(a))return Rr("Waiting for typing to pause"),Sr("Scene sync skipped (stale snapshot)."),void ec();const c=a.wordCount;if(!Number.isInteger(c)||c<0)return Rr("Sync failed"),Sr("Scene sync failed."),void ec();if(zn===a.sceneId&&Kn===c)return Rr("Synced"),Sr("Sync skipped (count unchanged)."),void ec();Sr("Scene word count changed."),ke&&(ke.disabled=!0,ke.textContent="Syncing..."),Rr("Syncing..."),Sr(`Scene sync started (${t}).`);try{const e=Ca(),t=await Wa("/api/word-companion/runtime/scene-word-count",{documentGuid:a.documentGuid||o,bookId:a.bookId||r.bookId,chapterId:a.chapterId,sceneId:a.sceneId,wordCount:c});if(br(`Word count sync API: ${Math.round(Ca()-e)}ms`),!ka(a))return void Sr("Scene sync result ignored (stale snapshot).");const i=t?.actualWords??c,s=new Date;zn=a.sceneId,Kn=i,Co(xe,zr(i)),Co(we,zr(i)),Co(Ne,Kr(s)),Co(Ce,Kr(s)),Rr("Synced"),Sr("Scene sync succeeded."),br(`Total word count sync: ${Math.round(Ca()-n)}ms`)}catch(e){Rr("Sync failed"),ko({lastError:vo(e)}),Sr("Scene sync failed.")}finally{ke&&(ke.textContent="Sync Current Scene"),ec()}},nc=(e=4e3)=>{Tr(),sn&&dn&&!Wn&&!Jn&&(Yn=window.setTimeout(()=>{Yn=null,tc({source:"automatic"})},e))},rc=()=>{Qn&&(window.clearTimeout(Qn),Qn=null)},oc=()=>{er=null,tr="",nr=[],rr=null,or="",rc()},ac=()=>{ar=null,cr="",ir=[],sr=null,dr=""},cc=()=>{lr=null,ur="",pr=[],hr=null,mr=""},ic=async(e=!1)=>{const t=Ao(),n=Yo();if(!t||!n)return nr=[],[];if(!e&&er===t.projectId&&tr===n&&nr.length>0)return nr;const r=await ja(`/api/word-companion/runtime/characters?projectId=${encodeURIComponent(t.projectId)}&documentGuid=${encodeURIComponent(n)}`);return er=t.projectId,tr=n,nr=Array.isArray(r?.characters)?r.characters:[],Sr("Character alias cache refreshed."),nr},sc=async(e=!1)=>{const t=Ao(),n=Yo();if(!t||!n)return ir=[],[];if(!e&&ar===t.projectId&&cr===n&&ir.length>0)return ir;const r=await ja(`/api/word-companion/runtime/assets?projectId=${encodeURIComponent(t.projectId)}&documentGuid=${encodeURIComponent(n)}`);return ar=t.projectId,cr=n,ir=Array.isArray(r?.assets)?r.assets:[],Sr(`Asset cache loaded: ${ir.length} aliases.`),ir},dc=async(e=!1)=>{const t=Ao(),n=Yo();if(!t||!n)return pr=[],[];if(!e&&lr===t.projectId&&ur===n&&pr.length>0)return pr;const r=await ja(`/api/word-companion/runtime/locations?projectId=${encodeURIComponent(t.projectId)}&documentGuid=${encodeURIComponent(n)}`);return lr=t.projectId,ur=n,pr=Array.isArray(r?.locations)?r.locations:[],Sr(`Location cache loaded: ${pr.length} aliases.`),pr},lc=(e,t)=>{const n=String(t||"").trim();if(!n)return!1;const r=(o=n,String(o||"").replace(/[.*+?^${}()|[\]\\]/g,"\\$&")).replace(/\s+/g,"\\s+");var o;return new RegExp(`(^|[^\\p{L}\\p{N}_])${r}(?=$|[^\\p{L}\\p{N}_])`,"iu").test(e)},uc=e=>{if(e?.excludeFromCompanionDetection)return!1;const t=Number.parseInt(e?.detectionPriority||"0",10);return n=e?.matchText,String(n||"").trim().split(/\s+/).filter(Boolean).length>=2||Number.isInteger(t)&&t>=75;var n},pc=async()=>{if(Qn=null,!sn||Wn||!Yo())return;if(Xn)return Zn=!0,void Sr("Character suggestion detection deferred; detection already running.");Xn=!0,Zn=!1;const e=Ca();try{const t=await xa();if(!ka(t))return void Sr("Runtime suggestions skipped (stale snapshot).");const[n,r,o]=await Promise.all([ic(),sc(),dc()]);if(!ka(t))return void Sr("Runtime suggestions skipped after cache load (stale snapshot).");const a=t.sceneText||"",c=Ca(),i=((e,t)=>{if(!e||!Array.isArray(t)||0===t.length)return[];const n=new Map;for(const r of t){const t=Number.parseInt(r.characterId||"",10);!Number.isInteger(t)||t<=0||n.has(t)||lc(e,r.matchText)&&n.set(t,r.name||r.matchText||"Character")}return[...n.entries()].map(([e,t])=>({characterId:e,name:t}))})(a,n);br(`Character detection: ${Math.round(Ca()-c)}ms`);const s=i.map(e=>e.characterId),d=s.slice().sort((e,t)=>e-t).join(","),l=Ca(),u=((e,t)=>{if(!e||!Array.isArray(t)||0===t.length)return[];const n=new Map;for(const r of t){const t=Number.parseInt(r.assetId||"",10);!Number.isInteger(t)||t<=0||n.has(t)||lc(e,r.matchText)&&n.set(t,r.name||r.matchText||"Asset")}return[...n.entries()].map(([e,t])=>({assetId:e,name:t}))})(a,r);br(`Asset detection: ${Math.round(Ca()-l)}ms`);const p=u.map(e=>e.assetId),h=p.slice().sort((e,t)=>e-t).join(","),m=Ca(),y=((e,t)=>{if(!e||!Array.isArray(t)||0===t.length)return[];const n=new Map;for(const r of t){const t=Number.parseInt(r.locationId||"",10);!Number.isInteger(t)||t<=0||n.has(t)||!uc(r)||lc(e,r.matchText)&&n.set(t,r.name||r.matchText||"Location")}return[...n.entries()].map(([e,t])=>({locationId:e,name:t}))})(a,o);br(`Location detection: ${Math.round(Ca()-m)}ms`);const g=y.map(e=>e.locationId),f=g.slice().sort((e,t)=>e-t).join(",");if(!d||rr===sn&&or===d)Sr("Character suggestions skipped.");else{if(!ka(t))return void Sr("Character suggestions skipped before submit (stale snapshot).");const e=Ca(),n=await Wa("/api/word-companion/runtime/character-suggestions",{documentGuid:t.documentGuid,sceneId:t.sceneId,characterIds:s});if(br(`Character suggestion API: ${Math.round(Ca()-e)}ms`),!ka(t))return void Sr("Character suggestion result ignored (stale snapshot).");if(rr=t.sceneId,or=d,(n?.createdCount||0)>0){const e=i.slice(0,3).map(e=>e.name).join(", "),t=i.length>3?" +"+(i.length-3):"";Pr(e?`Characters detected: ${e}${t}`:n.message)}Sr(n?.message||"Character suggestions submitted.")}if(!h||sr===sn&&dr===h)Sr("Asset suggestions skipped.");else{if(!ka(t))return void Sr("Asset suggestions skipped before submit (stale snapshot).");Sr(`Asset matches found: ${u.map(e=>e.name).join(", ")}`);const e=Ca(),n=await Wa("/api/word-companion/runtime/asset-suggestions",{documentGuid:t.documentGuid,sceneId:t.sceneId,assetIds:p});if(br(`Asset suggestion API: ${Math.round(Ca()-e)}ms`),!ka(t))return void Sr("Asset suggestion result ignored (stale snapshot).");if(sr=t.sceneId,dr=h,(n?.createdCount||0)>0){const e=u.slice(0,3).map(e=>e.name).join(", "),t=u.length>3?" +"+(u.length-3):"";Pr(e?`Assets detected: ${e}${t}`:n.message)}Sr(n?.message||"Asset suggestions submitted.")}if(!f||hr===sn&&mr===f)Sr("Location suggestions skipped.");else{if(!ka(t))return void Sr("Location suggestions skipped before submit (stale snapshot).");Sr(`Location matches found: ${y.map(e=>e.name).join(", ")}`);const e=Ca(),n=await Wa("/api/word-companion/runtime/location-suggestions",{documentGuid:t.documentGuid,sceneId:t.sceneId,locationIds:g});if(br(`Location suggestion API: ${Math.round(Ca()-e)}ms`),!ka(t))return void Sr("Location suggestion result ignored (stale snapshot).");if(hr=t.sceneId,mr=f,(n?.createdCount||0)>0){const e=y.slice(0,3).map(e=>e.name).join(", "),t=y.length>3?" +"+(y.length-3):"";Pr(e?`Locations detected: ${e}${t}`:n.message)}Sr(n?.message||"Location suggestions submitted.")}br(`Total suggestion pass: ${Math.round(Ca()-e)}ms`)}catch(e){console.error("Unable to detect runtime suggestions.",e),ko({lastError:vo(e)})}finally{Xn=!1,Zn&&(Zn=!1,hc(1500))}},hc=(e=2500)=>{rc(),!sn||Wn||Xn?Xn&&(Zn=!0):Qn=window.setTimeout(()=>{pc()},e)},mc=async()=>{const e=Ao(),t=$o(),n=yo(tn,"chapter");if(go({projectId:e?.projectId,projectTitle:e?.title,bookId:t?.bookId,bookTitle:t?qo(t):void 0,detectedChapter:tn,detectedScene:nn,requestChapter:n,requestScene:"",result:"Not run",chapterId:null,sceneId:null}),!t)return lo("Not detected"),Pr("Select a PlotDirector book first."),!1;if(!tn)return lo("Not detected"),Pr("No chapter detected in Word. Use Heading 1 for chapters."),!1;lo("Not detected");try{const e=await wo(t.bookId),r=an?e.find(e=>e.chapterId===an):null,o=n.toLocaleLowerCase(),a=o?e.find(e=>yo(e.title,"chapter").toLocaleLowerCase()===o):null,c=r||a;return c?(ao(),Jr(null,c.chapterId,r?"Chapter anchor":"Chapter title"),un=r?"ChapterID Anchor":"Title Match",qr("Chapter linked to PlotDirector"),Pr("No scene detected in Word. Use Heading 2 for scenes."),go({result:"Chapter matched",chapterId:c.chapterId,sceneId:null}),!0):(Jr(null),qr("Chapter not found in PlotDirector."),Pr("Chapter not found in PlotDirector."),go({result:"Chapter not matched",chapterId:null,sceneId:null}),co("Chapter not found in PlotDirector."),!1)}catch(e){return lo("Not found in PlotDirector"),ko({lastError:vo(e)}),Pr("Unable to load PlotDirector chapter data."),!1}},yc=async()=>{const e=Ao(),t=$o(),n=yo(tn,"chapter"),r=yo(nn,"scene");if(go({projectId:e?.projectId,projectTitle:e?.title,bookId:t?.bookId,bookTitle:t?qo(t):void 0,detectedChapter:tn,detectedScene:nn,requestChapter:n,requestScene:r,result:"Not run",chapterId:null,sceneId:null}),Dr({anchorType:cn?"SceneID Anchor":an?"ChapterID Anchor":"Title Match",storedSceneId:cn,storedChapterId:an,resolutionMethod:"-"}),!t)return lo("Not detected"),Pr("Select a PlotDirector book first."),go({result:"Error"}),!1;if(!tn||!nn)return lo("Not detected"),Pr("No scene detected in Word. Use Heading 2 for scenes."),go({result:"Error"}),!1;ce&&(ce.disabled=!0,ce.textContent="Refreshing...");const o=()=>{ce&&(ce.disabled=!1,ce.textContent="Refresh PlotDirector Scene")};lo("Not detected"),Pr("Resolving PlotDirector scene...");try{if(cn){const e=await(async(e,t)=>{const n=await ja(`/api/word-companion/books/${e}/structure`),r=Array.isArray(n?.chapters)?n.chapters:[];for(const e of r){const n=Array.isArray(e.scenes)?e.scenes:[];for(const r of n)if(t(e,r))return{chapter:e,scene:r}}return null})(t.bookId,(e,t)=>t.sceneId===cn);if(e)return go({result:"Matched",chapterId:e.chapter.chapterId,sceneId:e.scene.sceneId}),await bo(t.bookId,e.scene.sceneId,e.chapter.chapterId,"Anchor","SceneID Anchor"),!0;pn=!0,un="SceneID Anchor",qr("Not found in PlotDirector"),Pr("Stored PlotDirector ID is invalid."),go({result:"Error",chapterId:an,sceneId:cn}),Dr({anchorType:"SceneID Anchor",storedSceneId:cn,storedChapterId:an,resolutionMethod:"Anchor"})}if(an){const e=(await fo(t.bookId)).find(e=>e.chapterId===an),n=e?(Array.isArray(e.scenes)?e.scenes:[]).find(e=>yo(e.title,"scene").toLocaleLowerCase()===r.toLocaleLowerCase()):null;if(n)return go({result:"Matched",chapterId:e.chapterId,sceneId:n.sceneId}),await bo(t.bookId,n.sceneId,e.chapterId,"Anchor","ChapterID Anchor"),!0;e&&!cn?(un="ChapterID Anchor",Jr(null,an,"Chapter anchor"),go({result:"Chapter matched",chapterId:an,sceneId:null})):cn||(un="ChapterID Anchor",go({result:"Chapter anchor not found",chapterId:an,sceneId:null}))}const e=await Wa(`/api/word-companion/books/${t.bookId}/resolve-scene`,{chapterTitle:n,sceneTitle:r});if(!e?.matched||!e.sceneId){const t=pn,n=Number.parseInt(e?.chapterId||"",10),r=Number.isInteger(dn)&&dn>0?dn:null,a=Number.isInteger(n)&&n>0?n:r,c=!!e?.chapterMatched||!!a;return lo("Not found in PlotDirector"),pn=t,c&&(Jr(null,a,a===r?"Chapter anchor":"Chapter title"),qr("Scene not found in PlotDirector.")),Pr(t?"Stored PlotDirector ID is invalid.":c?"Scene not found in PlotDirector.":"This chapter does not exist in PlotDirector. Create or link the chapter first."),go({result:"Not matched",chapterId:c?a:e?.chapterId,sceneId:e?.sceneId}),t||(c?(ao(),so("Scene not found in PlotDirector.")):(io(),co("Chapter not found in PlotDirector."))),o(),!1}const a=pn;return go({result:a?"Matched by title fallback":"Matched",chapterId:e.chapterId,sceneId:e.sceneId}),await bo(t.bookId,e.sceneId,e.chapterId,a?"Title fallback":"Title","Title Match"),a&&(pn=!0,Pr("Stored PlotDirector ID is invalid."),Ar()),!0}catch(e){const t=pn;return ko({lastError:vo(e)}),lo("Not found in PlotDirector"),pn=t,Pr(t?"Stored PlotDirector ID is invalid.":"Unable to load PlotDirector scene data."),go({result:"Error"}),!1}finally{o()}},gc=async()=>{if(Rn=null,Vn)return On=!0,void Sr("Runtime update deferred; update already running.");if(!_t||!$o())return Un=!1,void Sr("Runtime update skipped.");if(!Qt||!Xt||!window.Word||"function"!=typeof window.Word.run)return Un=!1,Vr("Manual"),void Pr("Automatic scene tracking is unavailable in this Word version. Use Refresh Current Scene.");if(!Cn)return Un=!1,Vr("Manual"),void Pr("Document structure is not cached yet. Use Refresh Current Scene.");const e=Date.now()-Mn;if(Un&&e{Rn&&window.clearTimeout(Rn),Rn=window.setTimeout(gc,Math.max(0,e)),Vr("Waiting for typing to pause"),Sr("Runtime update scheduled.")},wc=()=>{Rn&&(window.clearTimeout(Rn),Rn=null),Un=!1,On=!1},Ic=()=>{((e="selection",t=1200)=>{if(Un=!0,Mn=Date.now(),Bn+=1,Gn=null,Tr(),rc(),Jn&&(_n=!0),Xn&&(Zn=!0),Sr(`${e} changed.`),Vn)return On=!0,void Vr("Waiting for typing to pause");fc(t)})("Selection")},Sc=()=>{if(Fn&&window.Office?.context?.document&&"function"==typeof window.Office.context.document.removeHandlerAsync&&window.Office?.EventType?.DocumentSelectionChanged)try{window.Office.context.document.removeHandlerAsync(window.Office.EventType.DocumentSelectionChanged,{handler:Ic}),Fn=!1}catch(e){console.warn("Unable to remove Word selection tracking handler.",e)}},bc=async(e,n)=>{if(Cn=null,Zr(),!e)return Kt=[],Lo(H,"Select a project first"),Lo(m,"Select a project first"),Lr(""),Eo(t,null),Jo(),lo("Not detected"),void Mr();Lo(H,"Loading books..."),Lr(""),lo("Not detected"),Mr("Select a book to analyse manuscript structure.");try{const r=await ja(`/api/word-companion/projects/${e}/books`);if(Kt=Array.isArray(r.books)?r.books:[],0===Kt.length)return Lo(H,"No books available."),Lo(m,"No books available."),Lr("No books available."),Eo(t,null),Jo(),void Oo("No books available.");Po(H,"Choose a book",Kt.map(e=>({...e,displayTitle:qo(e)})),"bookId","displayTitle");const o=Kt.find(e=>e.bookId===n);o&&H?(H.value=String(o.bookId),Eo(t,o.bookId)):Eo(t,null),((e=null)=>{if(!m)return;if(0===Kt.length)return void Lo(m,"No books available.");Po(m,"Choose a book",Kt.map(e=>({...e,displayTitle:qo(e)})),"bookId","displayTitle");const t=e||Number.parseInt(H?.value||"",10),n=Kt.find(e=>e.bookId===t);n&&(m.value=String(n.bookId)),Ro()})(o?.bookId||n),Jo(),lo(($o(),"Not detected")),Mr($o()?"Ready to analyse manuscript structure.":"Select a book to analyse manuscript structure."),Oo(jo()?"Ready to scan manuscript structure.":"Select a Book to begin.")}catch{Kt=[],Lo(H,"Unable to load books."),Lo(m,"Unable to load books."),Lr("Unable to load books."),Eo(t,null),Jo(),lo("Not detected"),Mr("Unable to load books."),Oo("Unable to load books.")}};for(const e of i)e.addEventListener("click",()=>vr(e.dataset.tabButton));xr(),G?.addEventListener("change",async()=>{const n=Number.parseInt(G.value||"",10);Xr(),h&&Number.isInteger(n)&&n>0&&(h.value=String(n)),Eo(e,Number.isInteger(n)&&n>0?n:null),Eo(t,null),lo("Not detected"),Mr(),Oo("Select a Book to begin."),await bc(n,null)}),H?.addEventListener("change",()=>{const e=Number.parseInt(H.value||"",10);Xr(),Eo(t,Number.isInteger(e)&&e>0?e:null),m&&Number.isInteger(e)&&e>0&&(m.value=String(e)),Jo(),lo("Not detected"),Mr($o()?"Ready to analyse manuscript structure.":"Select a book to analyse manuscript structure."),Oo(jo()?"Ready to scan manuscript structure.":"Select a Book to begin.")}),h?.addEventListener("change",async()=>{const n=Number.parseInt(h.value||"",10);Xr(),G&&Number.isInteger(n)&&n>0&&(G.value=String(n)),Eo(e,Number.isInteger(n)&&n>0?n:null),Eo(t,null),Oo("Select a Book to begin."),await bc(n,null)}),m?.addEventListener("change",()=>{const e=Number.parseInt(m.value||"",10);Xr(),H&&Number.isInteger(e)&&e>0&&(H.value=String(e)),Eo(t,Number.isInteger(e)&&e>0?e:null),Jo(),Oo(jo()?"Ready to scan manuscript structure.":"Select a Book to begin.")}),y?.addEventListener("input",Ro),g?.addEventListener("click",async()=>{const e=To(),t=String(y?.value||"").trim();if(e&&t){g.disabled=!0,Wo("Creating Book...");try{const n=await Wa(`/api/word-companion/projects/${e.projectId}/books`,{title:t});y&&(y.value=""),G&&(G.value=String(e.projectId)),await bc(e.projectId,n.bookId),m&&(m.value=String(n.bookId)),Wo("Ready to scan manuscript structure.")}catch(e){console.error("Unable to create Book from Word Companion.",e),Wo(`Unable to create Book: ${vo(e)}`)}finally{Ro()}}else Ro()}),f?.addEventListener("click",async()=>{const e=To(),t=jo();if(e&&t)if(Qt&&Xt&&window.Word&&"function"==typeof window.Word.run){f&&(f.disabled=!0,f.textContent="Scanning..."),Wo("Scanning manuscript structure...");try{const n=await window.Word.run(async e=>{const n=e.document.body.paragraphs;return n.load("text,styleBuiltIn,style"),await e.sync(),((e,t)=>{const n=[];let r=null,o=null,a=0;const c=[],i=new Set(["***","###"]),s=()=>{if(!r)return null;const e={title:`Scene ${r.scenes.length+1}`,sortOrder:10*(r.scenes.length+1),wordCount:0};return r.scenes.push(e),e};return e.forEach(e=>{const d=String(e.text||"").trim();if(!d)return;if(c.push(d),Ko(e,1))return r={title:d,sortOrder:10*(n.length+1),wordCount:0,scenes:[]},n.push(r),o=s(),void(a+=Qo(d));if(!r||!o)return;if(t&&i.has(d))return void(o=s());const l=Qo(d);r.wordCount+=l,o.wordCount+=l,a+=l}),{chapters:n,chapterCount:n.length,sceneCount:n.reduce((e,t)=>e+t.scenes.length,0),wordCount:a,documentText:c.join("\n")}})(n.items,!!t.usesExplicitScenes)});Nn=n,Ln=!1;const r=await Wa("/api/word-companion/manuscript/analyse",{projectId:e.projectId,bookId:t.bookId,documentGuid:Vo(),chapterCount:n.chapterCount,sceneCount:n.sceneCount,wordCount:n.wordCount});xn=r,(e=>{if(!S)return;S.innerHTML="";const t=document.createElement("strong");t.textContent="Manuscript Analysis",S.append(t);const n=document.createElement("dl"),r=[["Project",e.projectTitle],["Book",qo({title:e.bookTitle,subtitle:e.bookSubtitle})],["Chapters",Number(e.chapterCount||0).toLocaleString()],["Scenes",Number(e.sceneCount||0).toLocaleString()],["Words",Number(e.wordCount||0).toLocaleString()]];for(const[e,t]of r){const r=document.createElement("dt");r.textContent=e;const o=document.createElement("dd");o.textContent=t||"-",n.append(r,o)}S.append(n),jr(S,!1),jr(L,!1),Wo(e.message||"Preview generated."),Ro()})(r);try{const t=await Wa("/api/word-companion/manuscript/discover-characters",{projectId:e.projectId,documentText:n.documentText||""});En=Array.isArray(t?.candidates)?t.candidates:[],jr(b,!0),jr(v,!0)}catch(e){console.error("Unable to discover character candidates.",e),En=[],jr(b,!0),jr(v,!0)}ko({lastScan:"Success",lastError:"-",paragraphs:"-",heading1:String(n.chapterCount),heading2:"-"})}catch(e){console.error("Unable to scan manuscript for first-run import.",e),Oo(`Unable to scan manuscript: ${vo(e)}`),ko({lastScan:"Failure",lastError:vo(e)})}finally{f&&(f.textContent="Scan Manuscript"),Ro()}}else Oo("Word document access is unavailable.");else Oo("Select a Project and Book to begin.")}),P?.addEventListener("click",async()=>{const n=To(),o=jo();if(!(n&&o&&Nn&&xn?.canImport))return void Ro();let a=!1;if(!xn.requiresReplaceConfirmation||(a=await new Promise(e=>{if(!D||"function"!=typeof D.showModal)return void e(window.confirm("This Book already contains planned chapters and scenes.\n\nImporting this manuscript will replace the planned structure.\n\nContinue?"));const t=t=>{A?.removeEventListener("click",n),$?.removeEventListener("click",r),D?.removeEventListener("cancel",o),D?.close(),e(t)},n=()=>t(!0),r=()=>t(!1),o=e=>{e.preventDefault(),t(!1)};A?.addEventListener("click",n),$?.addEventListener("click",r),D?.addEventListener("cancel",o),D.showModal()}),a)){Dn=!0,Ro(),Wo("Importing manuscript structure...");try{const i=Vo(),s=await Wa("/api/word-companion/manuscript/import",{projectId:n.projectId,bookId:o.bookId,documentGuid:i,bindingVersion:1,replacePlanned:a,chapters:Nn.chapters});if(!s.imported)return xn={...xn,canImport:!s.requiresReplaceConfirmation,requiresReplaceConfirmation:!!s.requiresReplaceConfirmation,message:s.message||xn.message},Wo(s.message||"Import was not completed."),void Ro();const d=s.manuscriptDocument;await(c={documentGuid:d?.documentGuid||i,bookId:s.bookId,projectId:s.projectId,bindingVersion:d?.bindingVersion||1},new Promise((e,t)=>{const n=window.Office?.context?.document?.settings;n?(n.set(r.documentGuid,c.documentGuid),n.set(r.bookId,String(c.bookId)),n.set(r.projectId,String(c.projectId)),n.set(r.bindingVersion,String(c.bindingVersion||1)),n.saveAsync(n=>{n.status===window.Office.AsyncResultStatus.Succeeded?e():t(n.error||new Error("Unable to save Word document metadata."))})):t(new Error("Word document settings are unavailable."))})),Sn={documentGuid:d?.documentGuid||i,bookId:s.bookId,projectId:s.projectId,bindingVersion:d?.bindingVersion||1},Eo(e,s.projectId),Eo(t,s.bookId),G&&(G.value=String(s.projectId));const l=En;await bc(s.projectId,s.bookId),Nr("Connected"),Ln=!0,Pn=l.length>0,qn=Pn?Date.now():0,En=l,l.length>0?(Uo(!0),Go(l),jr(L,!0),jr(v,!1),Wo(s.message||"Manuscript structure imported."),Pr(s.message||"Manuscript structure imported."),window.setTimeout(Ro,750)):await Ra(s.message||"Manuscript structure imported.")}catch(e){console.error("Unable to import manuscript structure.",e),Wo(`Unable to import manuscript: ${vo(e)}`)}finally{Dn=!1,Ro()}var c}else Wo("Import cancelled.")}),x?.addEventListener("click",async()=>{const e=To()||Ao(),t=Bo();if(!Pn||Date.now()-qn<750||!e||0===t.length)Ro();else{An=!0,Ro(),Co(C,"Creating selected characters...");try{const n=await Wa("/api/word-companion/manuscript/create-discovered-characters",{projectId:e.projectId,candidateNames:t});Pr(n.message||"Characters created."),Co(C,`${n.createdCount||0} created, ${n.skippedCount||0} already existed.`),await Ra(n.message||"Characters created.")}catch(e){console.error("Unable to create discovered characters.",e),Co(C,`Unable to create characters: ${vo(e)}`)}finally{An=!1,Ro()}}}),N?.addEventListener("click",()=>Ra("Character creation skipped.")),E?.addEventListener("click",()=>Ra()),w?.addEventListener("click",()=>Uo(!1)),q?.addEventListener("click",()=>Oo("Import cancelled.")),_?.addEventListener("click",Na),ne?.addEventListener("click",Ea),re?.addEventListener("click",async()=>{const e=$o();if(!e||0===Or().length||$n)return void Br();if(!await Pa())return;$n=!0,Gr(),Ur("Applying structure sync...");const t={createdChapters:0,createdScenes:0,linkedChapters:0,linkedScenes:0,skipped:Array.isArray(In?.manualReview)?In.manualReview.length:0,failed:0},n=Or().filter(e=>"couldLink"===e.category&&"Chapter"===e.type),r=Or().filter(e=>"wouldCreateChapters"===e.category),o=Or().filter(e=>"couldLink"===e.category&&"Scene"===e.type),a=Or().filter(e=>"wouldCreateScenes"===e.category),c=[];let i="";const s=(e,t)=>{c.push({item:e,counterName:t})},d=async(e,n)=>{try{await(async e=>{if(!Qt||!Xt||!window.Word||"function"!=typeof window.Word.run)throw new Error("Word document access is unavailable.");await window.Word.run(async t=>{const n=await Sa(t),r="Chapter"===e.type?e.wordChapter:e.wordScene,o=Number.isInteger(r?.paragraphIndex)?n.bodyParagraphs[r.paragraphIndex]:null;if(!o)throw new Error("Unable to locate the Word heading.");if("Chapter"===e.type){const t=e.pdChapter?.chapterId;if(!t)throw new Error("Chapter ID is not available.");Ha(o,"PlotDirector Chapter",`PD-CHAPTER-${t}`,"PD-CHAPTER")}else{const t=e.pdScene?.sceneId;if(!t)throw new Error("Scene ID is not available.");Ha(o,"PlotDirector Scene",`PD-SCENE-${t}`,"PD-SCENE")}await t.sync()})})(e),qa(e,"Success"),t[n]+=1}catch(n){qa(e,"Failed",vo(n)),t.failed+=1}};try{for(const e of n)s(e,"linkedChapters");for(const n of r)try{await $a(e.bookId,n),s(n,"createdChapters")}catch(e){qa(n,"Failed",vo(e)),t.failed+=1}for(const e of o)s(e,"linkedScenes");for(const n of a)try{await Ta(e.bookId,n),s(n,"createdScenes")}catch(e){qa(n,"Failed",vo(e)),t.failed+=1}for(const e of c)await d(e.item,e.counterName);for(const e of In.manualReview||[])qa(e,"Skipped");Ia(In),i=(e=>`Structure Sync Complete. Created: ${e.createdChapters} Chapters, ${e.createdScenes} Scenes. Linked: ${e.linkedChapters} Chapters, ${e.linkedScenes} Scenes. Skipped: ${e.skipped} Manual Review items. Failed: ${e.failed} Items.`)(t),Ur(i)}finally{$n=!1,Gr(),i&&(Zr(),await Ea(),Ur(`${i} Preview refreshed.`))}}),bt?.addEventListener("click",()=>La(!0)),Ct?.addEventListener("click",()=>La(!1)),It?.addEventListener("cancel",e=>{e.preventDefault(),La(!1)}),ce?.addEventListener("click",yc),J?.addEventListener("click",async()=>{if(!$o())return Pr("Select a PlotDirector book first."),void lo("Not detected");wc(),_r(!0),Vr("Updating...");try{await Promise.all([ic(!0),sc(!0),dc(!0)]);if(!await Na())return;if(Yr(!1),!nn)return await mc(),void Vr("Current scene updated");await yc(),Vr("Current scene updated")}finally{_r(!1)}}),ke?.addEventListener("click",()=>tc({source:"manual"})),Re?.addEventListener("click",async()=>{if(sn)if(!Wn&&await(async()=>{if(!sn)return!1;if(!Qt||!Xt||!window.Word||"function"!=typeof window.Word.run)return Yr(!0,"The Word cursor is now in a different scene. Refresh Current Scene before saving."),!1;try{const e=await window.Word.run(async e=>await Sa(e)),t=e.currentChapter?.title||"",n=e.currentScene?.title||"",r=e.currentScene?.anchorId||null,o=e.currentChapter?.anchorId||null,a=Number.isInteger(r)&&r===sn,c=Number.isInteger(o)&&o===dn,i=yo(n,"scene").toLocaleLowerCase()===yo(gr||nn,"scene").toLocaleLowerCase(),s=yo(t,"chapter").toLocaleLowerCase()===yo(fr||tn,"chapter").toLocaleLowerCase();return a||i&&(c||s)?(Yr(!1),!0):(Yr(!0,"The Word cursor is now in a different scene. Refresh Current Scene before saving."),!1)}catch(e){return console.error("Unable to verify current Word scene before saving.",e),ko({lastError:vo(e)}),Yr(!0,"The Word cursor is now in a different scene. Refresh Current Scene before saving."),!1}})()){Re&&(Re.disabled=!0,Re.textContent="Saving...");try{await(e=`/api/word-companion/scenes/${sn}/links`,t={characterIds:Ba(Te),assetIds:Ba(je),locationIds:[],primaryLocationId:Ga()},ja(e,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)})),(()=>{const e=new Set(Ba(Te)),t=new Set(Ba(je));Ir={characters:Ir.characters.map(t=>({...t,linked:e.has(Number.parseInt(uo(t,"character"),10))})),assets:Ir.assets.map(e=>({...e,linked:t.has(Number.parseInt(uo(e,"asset"),10))})),locations:Ir.locations,primaryLocationId:Ga()}})(),eo("Scene links saved successfully."),ko({lastError:"-"})}catch(e){eo("Unable to save scene links."),ko({lastError:vo(e)})}finally{Re&&(Re.disabled=!sn||Wn,Re.textContent="Save Scene Links")}var e,t}else eo("The Word cursor is now in a different scene. Refresh Current Scene before saving.");else eo("Link a PlotDirector scene first.")}),pe?.addEventListener("click",async()=>{await Fa()}),he?.addEventListener("click",async()=>{console.debug("Word Companion unlink clicked"),Pr("Preparing to unlink manuscript...");const e=(()=>{const e=Sn||Ho();if(e)return e;const t=bn?.manuscriptDocument?.documentGuid||"",n=Number.parseInt(bn?.bookId||"",10),r=Number.parseInt(bn?.projectId||"",10);return t&&Number.isInteger(n)&&n>0&&Number.isInteger(r)&&r>0?{documentGuid:t,bookId:n,projectId:r,bindingVersion:Number.parseInt(bn?.manuscriptDocument?.bindingVersion||"",10)||1}:null})();if(!e)return void Pr("This Word document is not linked.");if(!e.documentGuid||!Number.isInteger(e.bookId)||e.bookId<=0)return void Pr("Unable to unlink: bound document metadata is incomplete.");if(await new Promise(e=>{if(!T||"function"!=typeof T.showModal)return void e(window.confirm("This will remove PlotDirector binding metadata from this Word document.\n\nIf PlotDirector is reachable, it will also unlink this Book in PlotDirector.\n\nYour manuscript text will not be changed.\n\nContinue?"));const t=t=>{j?.removeEventListener("click",n),W?.removeEventListener("click",r),T?.removeEventListener("cancel",o),T?.close(),e(t)},n=()=>t(!0),r=()=>t(!1),o=e=>{e.preventDefault(),t(!1)};j?.addEventListener("click",n),W?.addEventListener("click",r),T?.addEventListener("cancel",o),T.showModal()})){Pr("Unlinking manuscript..."),he&&(he.disabled=!0,he.textContent="Unlinking...");try{await Wa("/api/word-companion/manuscript/unlink",{documentGuid:e.documentGuid,bookId:e.bookId}),await Fo(),await Oa("Manuscript unlinked.")}catch(e){console.error("Unable to unlink manuscript binding.",e),Pr("Unable to unlink from PlotDirector.");if(window.confirm("PlotDirector could not be updated, but you can still remove the binding from this Word document.\n\nYou may need to unlink the Book manually in PlotDirector later.\n\nRemove binding from this Word document only?"))try{await Fo(),await Oa("Word metadata removed. Unlink the Book manually in PlotDirector if it still shows as linked.")}catch(e){console.error("Unable to remove Word document metadata.",e),Pr(`Unable to remove Word metadata: ${vo(e)}`)}else Pr(`Unable to unlink manuscript: ${vo(e)}`)}finally{he&&(he.disabled=!1,he.textContent="Unlink this Word document"),Wr()}}else Pr("Unlink cancelled.")}),Ge?.addEventListener("click",async()=>{const e=$o();if(!e||!tn||dn)return void co("Chapter not found in PlotDirector.");const t=mo(tn),n=await((e,t)=>(Co(yt,`"${e}"`),Co(gt,`"${t}"`),mt?new Promise(e=>{wn=e,mt.showModal?mt.showModal():mt.hidden=!1}):Promise.resolve(window.confirm(`Create PlotDirector chapter:\n\n"${e}"\n\nin book:\n\n"${t}"\n\n?`))))(t,qo(e)||"selected book");if(n){Ge&&(Ge.disabled=!0,Ge.textContent="Creating...");try{const n=Number.isInteger(on)&&on>0?10*on:null,r=await Wa(`/api/word-companion/books/${e.bookId}/chapters`,{title:t,sortOrder:n});if(!r?.chapterId)throw new Error("Chapter creation did not return a chapter ID.");Zr(),Jr(null,r.chapterId,"Manual chapter link");if(!await Va("Chapter created and linked successfully."))return;await Ya("Chapter created and linked successfully.")}catch(e){no("Unable to create chapter."),ko({lastError:vo(e)})}finally{Ge&&(Ge.disabled=!ro(),Ge.textContent="Create Chapter in PlotDirector")}}}),He?.addEventListener("click",async()=>{const e=$o();if(e&&tn&&!dn){He&&(He.disabled=!0,He.textContent="Loading...");try{gn=await wo(e.bookId),fn=null,dt&&(dt.value=""),Co(ht,""),_a(),st?.showModal?st.showModal():st&&(st.hidden=!1)}catch(e){no("Unable to load chapters."),ko({lastError:vo(e)})}finally{He&&(He.disabled=!ro(),He.textContent="Link to Existing Chapter")}}else co("Chapter not found in PlotDirector.")}),dt?.addEventListener("input",_a),ut?.addEventListener("click",async()=>{const e=gn.find(e=>e.chapterId===fn);if(e){ut&&(ut.disabled=!0,ut.textContent="Linking...");try{Jr(null,e.chapterId,"Manual chapter link");if(!await Va("Chapter linked successfully."))return;za(),await Ya("Chapter linked successfully.")}catch(e){Co(ht,"Unable to link chapter."),no("Unable to link chapter."),ko({lastError:vo(e)})}finally{ut&&(ut.disabled=!fn,ut.textContent="Link Chapter")}}else Co(ht,"Select a chapter.")}),pt?.addEventListener("click",za),ft?.addEventListener("click",()=>Ja(!0)),wt?.addEventListener("click",()=>Ja(!1)),mt?.addEventListener("cancel",e=>{e.preventDefault(),Ja(!1)}),_e?.addEventListener("click",async()=>{const e=$o();if(!e||!nn)return void so("Scene not found in PlotDirector.");const t=yo(nn,"scene"),n=yo(tn,"chapter"),r=await((e,t)=>(Co(ot,`"${e}"`),Co(at,`"${t}"`),rt?new Promise(e=>{yn=e,rt.showModal?rt.showModal():rt.hidden=!1}):Promise.resolve(window.confirm(`Create PlotDirector scene:\n\n"${e}"\n\nwithin chapter:\n\n"${t}"\n\n?`))))(t,n);if(r){_e&&(_e.disabled=!0,_e.textContent="Creating...");try{const n=await So();if(!n)return void so("This chapter does not exist in PlotDirector. Create or link the chapter first.");const r=await Wa(`/api/word-companion/books/${e.bookId}/chapters/${n.chapterId}/scenes`,{sceneTitle:t});if(!r?.sceneId||!r?.chapterId)throw new Error("Scene creation did not return a scene ID.");Zr(),await Ka(r.sceneId,r.chapterId,"Scene created and linked successfully.")}catch(e){to("Unable to create scene."),ko({lastError:vo(e)})}finally{_e&&(_e.disabled=!oo(),_e.textContent="Create Scene in PlotDirector")}}}),ze?.addEventListener("click",async()=>{const e=$o();if(e&&nn){ze&&(ze.disabled=!0,ze.textContent="Loading...");try{const t=await fo(e.bookId),n=Io(t);if(!n)return void so("This chapter does not exist in PlotDirector. Create or link the chapter first.");hn=Array.isArray(n.scenes)?n.scenes:[],mn=null,Xe&&(Xe.value=""),Co(nt,""),Xa(),Qe?.showModal?Qe.showModal():Qe&&(Qe.hidden=!1)}catch(e){to("Unable to load scenes."),ko({lastError:vo(e)})}finally{ze&&(ze.disabled=!oo(),ze.textContent="Link to Existing Scene")}}else so("Scene not found in PlotDirector.")}),Xe?.addEventListener("input",Xa),et?.addEventListener("click",async()=>{const e=$o(),t=hn.find(e=>e.sceneId===mn);if(e&&t){et&&(et.disabled=!0,et.textContent="Linking...");try{const e=await So();if(!e)throw new Error("This chapter does not exist in PlotDirector. Create or link the chapter first.");Za(),await Ka(t.sceneId,e.chapterId,"Scene linked successfully.")}catch(e){Co(nt,"Unable to link scene."),to("Unable to link scene."),ko({lastError:vo(e)})}finally{et&&(et.disabled=!mn,et.textContent="Link Scene")}}else Co(nt,"Select a scene.")}),tt?.addEventListener("click",Za),ct?.addEventListener("click",()=>Qa(!0)),it?.addEventListener("click",()=>Qa(!1)),rt?.addEventListener("cancel",e=>{e.preventDefault(),Qa(!1)}),Tt?.addEventListener("click",async()=>{Tt&&(Tt.disabled=!0,Tt.textContent="Running...");try{if(await xo(),!Qt||!Xt||!window.Word||"function"!=typeof window.Word.run)return void ko({lastScan:"Failure",lastError:"Word document access is unavailable.",paragraphs:"-",heading1:"-",heading2:"-"});const e=await window.Word.run(async e=>{const t=e.document.body.paragraphs;return t.load("items"),await e.sync(),t.items.length});ko({lastScan:"Success",lastError:"-",paragraphs:String(e)})}catch(e){console.error("Word Companion diagnostics failed.",e);const t=vo(e);ko({lastScan:"Failure",lastError:t}),Pr(`Unable to scan the Word document: ${t}`)}finally{Tt&&(Tt.disabled=!1,Tt.textContent="Run Diagnostics")}});const Cc=_t?(async()=>{Lo(G,"Loading projects..."),Lo(H,"Select a project first"),Lo(h,"Loading projects..."),Lo(m,"Select a project first"),Er(""),Lr("");try{const n=await ja("/api/word-companion/projects");if(zt=Array.isArray(n.projects)?n.projects:[],0===zt.length)return Lo(G,"No projects available."),Lo(h,"No projects available."),Er("No projects available."),Eo(e,null),Eo(t,null),Jo(),void Oo("No projects available.");Po(G,"Choose a project",zt,"projectId","title"),Mo();const r=No(e),o=zt.find(e=>e.projectId===r);o&&G?(G.value=String(o.projectId),h&&(h.value=String(o.projectId)),Eo(e,o.projectId),await bc(o.projectId,No(t))):(Eo(e,null),Eo(t,null),Jo(),Oo("Select a Project and Book to begin."))}catch{zt=[],Kt=[],Lo(G,"Unable to connect to PlotDirector."),Lo(H,"Select a project first"),Lo(h,"Unable to connect to PlotDirector."),Lo(m,"Select a project first"),Nr("Unable to connect to PlotDirector."),Er("Unable to connect to PlotDirector."),Eo(e,null),Eo(t,null),Jo(),Oo("Unable to connect to PlotDirector.")}})():Promise.resolve();if(_t||(Lo(G,"Please sign in to PlotDirector."),Lo(H,"Please sign in to PlotDirector."),Lo(h,"Please sign in to PlotDirector."),Lo(m,"Please sign in to PlotDirector."),Jo()),!window.Office||"function"!=typeof window.Office.onReady)return Cr("Word Companion ready"),Pr("Word document access is unavailable."),Vr("Manual"),void ko({host:"Unavailable",platform:"Unavailable",ready:"No",wordApi:"No"});xo().then(async()=>{Cr("Word Companion ready"),await Cc,await(async()=>{if(!_t||!Xt)return void Uo(!1);const n=Ho();if(n)try{const r=await ja(`/api/word-companion/runtime/book/${n.bookId}?documentGuid=${encodeURIComponent(n.documentGuid)}`),o=r?.manuscriptDocument;if(r?.bookId===n.bookId&&r?.projectId===n.projectId&&o&&String(o.documentGuid).toLowerCase()===n.documentGuid.toLowerCase())return Sn=n,Qr(r),Eo(e,r.projectId),Eo(t,r.bookId),G&&(G.value=String(r.projectId)),await bc(r.projectId,r.bookId),Uo(!1),Nr("Connected"),Lr("Bound manuscript detected."),void await Ma()}catch{}Sn=null,Xr(),Mo(),To()?await bc(To().projectId,null):Lo(m,"Select a project first"),Oo("Select a Project and Book to begin."),Uo(!0)})(),Xt?(()=>{if(_t){if(!window.Office?.context?.document||"function"!=typeof window.Office.context.document.addHandlerAsync||!window.Office?.EventType?.DocumentSelectionChanged)return Vr("Manual"),void Pr("Automatic scene tracking is unavailable in this Word version. Use Refresh Current Scene.");try{window.Office.context.document.addHandlerAsync(window.Office.EventType.DocumentSelectionChanged,Ic,e=>{e?.status===window.Office.AsyncResultStatus.Succeeded?(Fn=!0,Vr("Live")):(Vr("Manual"),Pr("Automatic scene tracking is unavailable in this Word version. Use Refresh Current Scene."))}),window.addEventListener("beforeunload",Sc)}catch(e){console.warn("Unable to register Word selection tracking handler.",e),Vr("Manual"),Pr("Automatic scene tracking is unavailable in this Word version. Use Refresh Current Scene.")}}})():(Pr("Word document access is unavailable."),Vr("Manual"))}).catch(e=>{console.error("Office readiness check failed.",e),Nr("Unable to connect to PlotDirector."),Cr("Word Companion unavailable"),Pr("Word document access is unavailable."),ko({ready:"No",wordApi:"No",lastScan:"Failure"})})})(); \ No newline at end of file +(()=>{const s={projectId:"plotdirector.word.projectId",bookId:"plotdirector.word.bookId",activeTab:"plotdirector.word.activeTab"},wi={documentGuid:"plotdirector.word.documentGuid",bookId:"plotdirector.word.boundBookId",projectId:"plotdirector.word.boundProjectId",bindingVersion:"plotdirector.word.bindingVersion"},hl=new Set(["scene","review","maintenance","diagnostics"]),ws=document.querySelector(".word-companion-shell"),cp=document.querySelector(".word-companion-tabs"),bs=[...document.querySelectorAll("[data-tab-button]")],cl=[...document.querySelectorAll("[data-tab-panel]")],lp=document.querySelector("[data-linked-manuscript-card]"),ap=document.querySelector("[data-linked-project-title]"),vp=document.querySelector("[data-linked-book-title]"),yp=document.querySelector("[data-first-run-wizard]"),ut=document.querySelector("[data-first-run-project-select]"),d=document.querySelector("[data-first-run-book-select]"),bf=document.querySelector("[data-first-run-new-book-title]"),uo=document.querySelector("[data-first-run-create-book]"),er=document.querySelector("[data-first-run-scan]"),pp=document.querySelector("[data-first-run-cancel]"),wp=document.querySelector("[data-first-run-status]"),or=document.querySelector("[data-first-run-summary]"),yu=document.querySelector("[data-first-run-character-section]"),pu=document.querySelector("[data-first-run-character-status]"),bi=document.querySelector("[data-first-run-character-candidates]"),wu=document.querySelector("[data-first-run-character-actions]"),fo=document.querySelector("[data-first-run-create-characters]"),ll=document.querySelector("[data-first-run-skip-characters]"),al=document.querySelector("[data-first-run-continue]"),ks=document.querySelector("[data-first-run-import-actions]"),ds=document.querySelector("[data-first-run-import]"),bp=document.querySelector("[data-first-run-import-cancel]"),bu=document.querySelector("[data-first-run-replace-dialog]"),vl=document.querySelector("[data-first-run-replace-confirm]"),yl=document.querySelector("[data-first-run-replace-cancel]"),ku=document.querySelector("[data-unlink-word-document-dialog]"),pl=document.querySelector("[data-unlink-word-document-confirm]"),wl=document.querySelector("[data-unlink-word-document-cancel]"),bl=document.querySelector("[data-office-status]"),kl=document.querySelector("[data-connection-status]"),dl=document.querySelector("[data-summary-connection]"),gl=document.querySelector("[data-summary-project]"),na=document.querySelector("[data-summary-book]"),p=document.querySelector("[data-project-select]"),ft=document.querySelector("[data-book-select]"),kp=document.querySelector("[data-runtime-book-selectors]"),ta=document.querySelector("[data-project-message]"),ia=document.querySelector("[data-book-message]"),eo=document.querySelector("[data-refresh-current-scene]"),gr=document.querySelector("[data-refresh-document]"),ra=document.querySelector("[data-current-chapter]"),ua=document.querySelector("[data-current-scene]"),fa=document.querySelector("[data-current-word-count]"),ea=document.querySelector("[data-scene-tracking-status]"),oa=document.querySelector("[data-document-message]"),sa=document.querySelector("[data-sync-note]"),kf=document.querySelector("[data-document-outline]"),sr=document.querySelector("[data-analyse-manuscript-structure]"),gs=document.querySelector("[data-apply-structure-sync]"),dp=document.querySelector("[data-structure-preview-status]"),du=document.querySelector("[data-structure-preview-results]"),nu=document.querySelector("[data-refresh-plotdirector-scene]"),ha=document.querySelector("[data-plotdirector-scene-status]"),gp=document.querySelector("[data-anchor-status]"),nw=document.querySelector("[data-anchor-book-id]"),tw=document.querySelector("[data-anchor-chapter-id]"),iw=document.querySelector("[data-anchor-scene-id]"),ai=document.querySelector("[data-attach-plotdirector-ids]"),hr=document.querySelector("[data-unlink-word-document]"),ca=document.querySelector("[data-plotdirector-scene-details]"),la=document.querySelector("[data-plotdirector-scene-title]"),aa=document.querySelector("[data-plotdirector-revision-status]"),va=document.querySelector("[data-plotdirector-estimated-words]"),nh=document.querySelector("[data-plotdirector-actual-words]"),ya=document.querySelector("[data-plotdirector-blocked-status]"),th=document.querySelector("[data-plotdirector-blocked-reason-row]"),ih=document.querySelector("[data-plotdirector-blocked-reason]"),pa=document.querySelector("[data-runtime-last-sync]"),cr=document.querySelector("[data-sync-scene-progress]"),rw=document.querySelector("[data-sync-word-count]"),rh=document.querySelector("[data-sync-plotdirector-count]"),wa=document.querySelector("[data-sync-last-synced]"),uw=document.querySelector("[data-sync-status]"),ba=document.querySelector("[data-writing-brief-block]"),ka=document.querySelector("[data-writing-brief]"),da=document.querySelector("[data-plotdirector-link-groups]"),fw=document.querySelector("[data-character-review-summary]"),ew=document.querySelector("[data-asset-review-summary]"),ow=document.querySelector("[data-location-review-summary]"),df=document.querySelector("[data-character-chips]"),gf=document.querySelector("[data-asset-chips]"),ott=document.querySelector("[data-location-chips]"),lr=document.querySelector("[data-primary-location-select]"),ki=document.querySelector("[data-save-scene-links]"),sw=document.querySelector("[data-scene-links-status]"),ga=document.querySelector("[data-links-editing-scene]"),nv=document.querySelector("[data-chapter-create-link]"),hw=document.querySelector("[data-chapter-create-link-chapter]"),vt=document.querySelector("[data-create-plotdirector-chapter]"),yt=document.querySelector("[data-link-existing-chapter]"),cw=document.querySelector("[data-chapter-create-link-status]"),tv=document.querySelector("[data-scene-create-link]"),lw=document.querySelector("[data-create-link-chapter]"),aw=document.querySelector("[data-create-link-scene]"),pt=document.querySelector("[data-create-plotdirector-scene]"),wt=document.querySelector("[data-link-existing-scene]"),vw=document.querySelector("[data-create-link-status]"),ar=document.querySelector("[data-link-existing-dialog]"),oo=document.querySelector("[data-link-scene-search]"),so=document.querySelector("[data-link-scene-options]"),ei=document.querySelector("[data-link-selected-scene]"),yw=document.querySelector("[data-link-dialog-cancel]"),uh=document.querySelector("[data-link-dialog-status]"),di=document.querySelector("[data-create-scene-dialog]"),pw=document.querySelector("[data-create-dialog-scene]"),ww=document.querySelector("[data-create-dialog-chapter]"),bw=document.querySelector("[data-create-dialog-confirm]"),kw=document.querySelector("[data-create-dialog-cancel]"),vr=document.querySelector("[data-link-existing-chapter-dialog]"),ho=document.querySelector("[data-link-chapter-search]"),co=document.querySelector("[data-link-chapter-options]"),oi=document.querySelector("[data-link-selected-chapter]"),dw=document.querySelector("[data-link-chapter-dialog-cancel]"),fh=document.querySelector("[data-link-chapter-dialog-status]"),gi=document.querySelector("[data-create-chapter-dialog]"),gw=document.querySelector("[data-create-chapter-dialog-chapter]"),nb=document.querySelector("[data-create-chapter-dialog-book]"),tb=document.querySelector("[data-create-chapter-dialog-confirm]"),ib=document.querySelector("[data-create-chapter-dialog-cancel]"),nr=document.querySelector("[data-apply-structure-sync-dialog]"),eh=document.querySelector("[data-apply-structure-sync-summary]"),rb=document.querySelector("[data-apply-structure-sync-confirm]"),ub=document.querySelector("[data-apply-structure-sync-cancel]"),fb=document.querySelector("[data-resolve-project-id]"),eb=document.querySelector("[data-resolve-project-title]"),ob=document.querySelector("[data-resolve-book-id]"),sb=document.querySelector("[data-resolve-book-title]"),hb=document.querySelector("[data-resolve-detected-chapter]"),cb=document.querySelector("[data-resolve-detected-scene]"),lb=document.querySelector("[data-resolve-request-chapter]"),ab=document.querySelector("[data-resolve-request-scene]"),vb=document.querySelector("[data-resolve-result]"),yb=document.querySelector("[data-resolve-chapter-id]"),pb=document.querySelector("[data-resolve-scene-id]"),tu=document.querySelector("[data-run-diagnostics]"),wb=document.querySelector("[data-diagnostic-host]"),bb=document.querySelector("[data-diagnostic-platform]"),kb=document.querySelector("[data-diagnostic-ready]"),db=document.querySelector("[data-diagnostic-word-api]"),gb=document.querySelector("[data-diagnostic-last-scan]"),nk=document.querySelector("[data-diagnostic-last-error]"),tk=document.querySelector("[data-diagnostic-anchor-type]"),ik=document.querySelector("[data-diagnostic-stored-scene-id]"),rk=document.querySelector("[data-diagnostic-stored-chapter-id]"),uk=document.querySelector("[data-diagnostic-resolution-method]"),fk=document.querySelector("[data-diagnostic-paragraphs]"),ek=document.querySelector("[data-diagnostic-heading1]"),ok=document.querySelector("[data-diagnostic-heading2]"),gu=ws?.dataset.authenticated==="true",oh=Number.parseInt(ws?.dataset.userId||"",10),sk=String(ws?.dataset.companionVersion||"20C").trim();let tr=[],ti=[],bt=!1,et=!1,lo="Unknown",ao="Unknown",w="",tt="",iv=null,ne=null,b=null,g=null,u=null,e=null,te="",ir="Title Match",kt=!1,sh=[],ie=null,vo=null,hh=[],re=null,yo=null,vi=null,ii=null,rr=null,l=null,ue=null,po=null,yr=null,fe=null,si=[],wo=!1,nf=!1,ee=0,oe=!1,ch=!1,se=!1,bo=null,lh="Manual",ot=!1,iu=null,pr=!1,rv=0,tf=!1,he=0,ce=null;const le=1200;let ah=!1,ko=!1,ae=null,rf=!1,ve=!1,vh=null,yh=null,ye=null,uf=!1,ff=!1,ph=null,wh="",ef=[],bh=null,kh="",dh=null,gh="",ru=[],nc=null,tc="",ic=null,rc="",uu=[],uc=null,fc="",ec="",go="",oc="",uv=0,hi=null,fu=null,st={characters:[],assets:[],locations:[],primaryLocationId:null};const i=n=>{const t=Date.now();t-uv<1e3||(uv=t,console.debug(`[Word Companion Runtime] ${n}`))},ci=n=>{console.debug(`[Word Companion Timing] ${n}`)},sc=n=>{bl&&(bl.textContent=n)},hk=n=>{try{return window.localStorage.getItem(n)||""}catch{return""}},ck=(n,t)=>{try{t?window.localStorage.setItem(n,String(t)):window.localStorage.removeItem(n)}catch{}},fv=n=>n==="links"?"review":n==="structure"?"maintenance":n,ev=(n,t=true)=>{const i=fv(n),r=hl.has(i)&&bs.some(n=>n.dataset.tabButton===i)?i:"scene";for(const n of bs){const t=n.dataset.tabButton===r;n.classList.toggle("is-active",t);n.setAttribute("aria-selected",t?"true":"false")}for(const n of cl){const t=n.dataset.tabPanel===r;n.classList.toggle("is-active",t);n.hidden=!t}t&&ck(s.activeTab,r)},ov=()=>{const n=fv(hk(s.activeTab));ev(hl.has(n)?n:"scene",!1)},pe=n=>{kl&&(kl.textContent=n),dl&&(dl.textContent=n)},hc=n=>{ta&&(ta.textContent=n)},sf=n=>{ia&&(ia.textContent=n)},t=n=>{oa&&(oa.textContent=n)},hf=n=>{ha&&(ha.textContent=n)},cc=t=>{const i=n=>Object.prototype.hasOwnProperty.call(t,n);i("anchorType")&&n(tk,a(t.anchorType));i("storedSceneId")&&n(ik,a(t.storedSceneId));i("storedChapterId")&&n(rk,a(t.storedChapterId));i("resolutionMethod")&&n(uk,a(t.resolutionMethod))},ur=()=>{const i=h(),f=Number.isInteger(u)&&u>0&&g===u,o=!Number.isInteger(e)||e<=0||b===e,t=f&&o;n(gp,t?"Attached to PlotDirector":"Not attached");n(nw,i?.bookId?String(i.bookId):"-");n(tw,Number.isInteger(e)&&e>0?String(e):"-");n(iw,Number.isInteger(u)&&u>0?String(u):"-");ai&&(r(ai,t&&!kt),ai.disabled=!u||t&&!kt,ai.textContent=g&&!t?"Reattach PlotDirector IDs":"Attach PlotDirector IDs");cc({anchorType:ir||"Title Match",storedSceneId:g,storedChapterId:b,resolutionMethod:te})},cf=(t,i,r,u=null,f=null,e=null)=>{const o=t||"",s=i||"",h=Number.isInteger(r)?r:null,c=Number.isInteger(e)&&e>0?e:null,l=Number.isInteger(u)&&u>0?u:null,a=Number.isInteger(f)&&f>0?f:null,v=w===o&&tt===s&&iv===h&&ne===c&&b===l&&g===a;(w=t||"",tt=i||"",iv=Number.isInteger(r)?r:null,ne=Number.isInteger(e)&&e>0?e:null,b=Number.isInteger(u)&&u>0?u:null,g=Number.isInteger(f)&&f>0?f:null,v)||(ra&&(ra.textContent=t||"Not detected"),ua&&(ua.textContent=i||"Not detected"),fa&&(fa.textContent=Number.isInteger(r)?String(r):"-"),n(rw,Number.isInteger(r)?String(r):"-"),ur())},lf=()=>{ae&&(window.clearTimeout(ae),ae=null)},r=(n,t)=>{n&&(n.hidden=t)},lc=()=>{r(hr,!ii)},dt=t=>{n(uw,t)},fr=t=>{n(dp,t)},af=(n="Select a project and book to analyse manuscript structure.")=>{if(vi=null,du){du.innerHTML="";const n=document.createElement("p");n.textContent="No preview generated.";du.append(n)}fr(n);ns()},lk=(n=vi)=>sy.flatMap(t=>Array.isArray(n?.[t.key])?n[t.key]:[]),eu=(n=vi)=>lk(n).filter(n=>n.category==="couldLink"||n.category==="wouldCreateChapters"||n.category==="wouldCreateScenes"),ns=()=>{gs&&(gs.disabled=se||eu().length===0)},ts=()=>{sr&&(sr.disabled=se||!gt()||!h()),ns()},ac=()=>{ga&&(ga.textContent=u?`Editing links for: ${go||tt||"Current scene"}`:"Link a PlotDirector scene first.")},vc=()=>{const n=ot||!u,t=n||!e||rf;cr&&(cr.disabled=t);ki&&(ki.disabled=n);lr&&(lr.disabled=n);for(const t of[df,gf])for(const i of t?[...t.querySelectorAll("input[type='checkbox']")]:[])i.disabled=n},k=t=>{(t==="Live"||t==="Manual")&&(lh=t),n(ea,t||lh||"Manual")},li=(i,r="")=>{ot=!!i,ot&&(lf(),io()),n(ea,ot?"Stale":lh),vc(),r&&(t(r),su(r))},wr=(n,t=null,i="")=>{u=Number.isInteger(n)&&n>0?n:null,e=Number.isInteger(t)&&t>0?t:null,te=i||"",u!==vh&&(yh=null),u||(lf(),io()),vc(),dt(ot?"Refresh Current Scene before syncing.":u?"Ready to sync.":"No resolved PlotDirector scene."),su(ot?"Refresh Current Scene before saving.":u?"Ready to save.":"Link a PlotDirector scene first."),ac(),ur()},sv=n=>{eo&&(eo.disabled=n,eo.textContent=n?"Refreshing...":"Refresh Current Scene")},a=n=>n===null||n===undefined||n===""?"-":String(n),stt=n=>{if(!n)return"-";const t=new Date(n);return Number.isNaN(t.getTime())?"-":t.toLocaleString()},is=n=>{if(!n)return"-";const t=n instanceof Date?n:new Date(n);if(Number.isNaN(t.getTime()))return"-";const i=new Date,r=t.toDateString()===i.toDateString()?"Today":t.toLocaleDateString();return`Last synced: ${r} ${t.toLocaleTimeString([],{hour:"2-digit",minute:"2-digit"})}`},hv=t=>{rr=t||null;const i=rr?.lastSyncUtc||rr?.manuscriptDocument?.lastSyncUtc;n(pa,is(i));n(wa,is(i));wc()},vf=()=>{l=null,ue=null,po=null,he+=1,ce=null,cn(),ln(),an(),hv(null)},ou=()=>{ue=null,po=null},su=t=>{n(sw,t)},hu=t=>{n(vw,t)},yi=t=>{n(cw,t)},yc=()=>!!h()&&!!w&&!e,pc=()=>!!h()&&!!tt&&Number.isInteger(e)&&e>0,we=()=>{r(nv,!0),yi(""),vt&&(vt.disabled=!0,vt.textContent="Create Chapter in PlotDirector"),yt&&(yt.disabled=!0,yt.textContent="Link to Existing Chapter")},rs=(t="")=>{n(hw,a(w));r(nv,!1);yi(t);const i=yc();vt&&(vt.disabled=!i,vt.textContent="Create Chapter in PlotDirector");yt&&(yt.disabled=!i,yt.textContent="Link to Existing Chapter")},us=()=>{r(tv,!0),hu(""),pt&&(pt.disabled=!0,pt.textContent="Create Scene in PlotDirector"),wt&&(wt.disabled=!0,wt.textContent="Link to Existing Scene")},be=(t="")=>{n(lw,a(w));n(aw,a(tt));r(tv,!1);hu(t);const i=pc();pt&&(pt.disabled=!i,pt.textContent="Create Scene in PlotDirector");wt&&(wt.disabled=!i,wt.textContent="Link to Existing Scene")},nt=t=>{hf(t),r(ca,!0),r(ba,!0),r(da,!1),we(),us(),te="",go="",oc="",ir=g?"SceneID Anchor":b?"ChapterID Anchor":"Title Match",kt=!1,st={characters:[],assets:[],locations:[],primaryLocationId:null},es(df,[],"character"),es(gf,[],"asset"),cv([],null,!1),gv(),wr(null),n(rh,"-"),n(la,"-"),n(aa,"-"),n(va,"-"),n(nh,"-"),n(ya,"-"),n(ih,"-"),r(th,!0),ur(),ac()},fs=(n,t)=>t==="asset"?n?.assetId||n?.AssetId||n?.storyAssetId||n?.StoryAssetId||0:t==="location"?n?.locationId||n?.LocationId||0:n?.characterId||n?.CharacterId||0,es=(n,t,i,r=false)=>{if(n){n.innerHTML="";const u=Array.isArray(t)?t:[];if(u.length===0){const t=document.createElement("p");t.className="word-companion-empty-list";t.textContent="None";n.append(t);return}for(const t of u){const f=Number.parseInt(fs(t,i),10);if(Number.isInteger(f)&&!(f<=0)){const e=document.createElement("label");e.className="word-companion-check-item";const u=document.createElement("input");u.type="checkbox";u.value=String(f);u.checked=!!t.linked;u.disabled=!r;const o=document.createElement("span");o.textContent=t.name||"Unnamed";e.append(u,o);n.append(e)}}}},cv=(n,t,i=false)=>{if(lr){lr.innerHTML="";const r=document.createElement("option");r.value="";r.textContent="None";lr.append(r);const u=Number.parseInt(t||"",10);for(const t of Array.isArray(n)?n:[]){const n=Number.parseInt(fs(t,"location"),10);if(Number.isInteger(n)&&!(n<=0)){const i=document.createElement("option");i.value=String(n);i.textContent=t.name||"Unnamed";i.selected=n===u;lr.append(i)}}lr.disabled=!i}},ak=t=>{go=t?.sceneTitle||tt||"",oc=t?.chapterTitle||w||"",n(la,a(t?.sceneTitle)),n(aa,a(t?.revisionStatus||t?.revisionStatusName||"Not provided")),n(va,a(t?.estimatedWords)),n(nh,a(t?.actualWords)),n(rh,a(t?.actualWords)),n(ya,t?.blocked?"Blocked":"Not blocked"),t?.blockedReason?(n(ih,t.blockedReason),r(th,!1)):(n(ih,"-"),r(th,!0)),ka&&(ka.textContent=t?.writingBrief||"No writing brief has been added for this scene."),st={characters:Array.isArray(t?.characters)?t.characters:[],assets:Array.isArray(t?.assets)?t.assets:[],locations:Array.isArray(t?.locations)?t.locations:[],primaryLocationId:t?.primaryLocationId??null},es(df,st.characters,"character",!!u&&!ot),es(gf,st.assets,"asset",!!u&&!ot),cv(st.locations,st.primaryLocationId,!!u&&!ot),gv(),ac(),su(ot?"Refresh Current Scene before saving.":u?"Ready to save.":"No resolved PlotDirector scene."),r(ca,!1),r(ba,!1),r(da,!1)},cu=n=>String(n||"").trim().replace(/\s+/g," "),it=(n,t)=>{const i=cu(n);if(!i)return"";const r=t==="scene"?"scene":"chapter",u=new RegExp(`^${r}\\s+${"(?:\\d+|[ivxlcdm]+|one|two|three|four|five|six|seven|eight|nine|ten|eleven|twelve|thirteen|fourteen|fifteen|sixteen|seventeen|eighteen|nineteen|twenty)"}\\s*(?::|-|\\u2013|\\u2014)\\s*`,"i"),f=cu(i.replace(u,""));return f||i},at=t=>{const i=n=>Object.prototype.hasOwnProperty.call(t,n);i("projectId")&&n(fb,a(t.projectId));i("projectTitle")&&n(eb,a(t.projectTitle));i("bookId")&&n(ob,a(t.bookId));i("bookTitle")&&n(sb,a(t.bookTitle));i("detectedChapter")&&n(hb,a(t.detectedChapter));i("detectedScene")&&n(cb,a(t.detectedScene));i("requestChapter")&&n(lb,a(t.requestChapter));i("requestScene")&&n(ab,a(t.requestScene));i("result")&&n(vb,a(t.result));i("chapterId")&&n(yb,a(t.chapterId));i("sceneId")&&n(pb,a(t.sceneId))},vk=async(n,t)=>{const i=await lt(`/api/word-companion/books/${n}/structure`),r=Array.isArray(i?.chapters)?i.chapters:[];for(const n of r){const r=Array.isArray(n.scenes)?n.scenes:[],i=r.find(n=>n.sceneId===t);if(i)return i.revisionStatus||""}return""},yk=async(n,t)=>{const i=await lt(`/api/word-companion/books/${n}/structure`),r=Array.isArray(i?.chapters)?i.chapters:[];for(const n of r){const i=Array.isArray(n.scenes)?n.scenes:[];for(const r of i)if(t(n,r))return{chapter:n,scene:r}}return null},os=async n=>{if(po===n&&Array.isArray(ue?.chapters))return ue.chapters;const t=await lt(`/api/word-companion/books/${n}/structure`);return po=n,ue=t||null,Array.isArray(t?.chapters)?t.chapters:[]},lv=async n=>{const t=await lt(`/api/word-companion/books/${n}/chapters`);return Array.isArray(t?.chapters)?t.chapters:[]},av=n=>{if(Number.isInteger(e)&&e>0){const t=n.find(n=>n.chapterId===e);if(t)return t}const t=it(w,"chapter").toLocaleLowerCase();return t?n.find(n=>it(n.title,"chapter").toLocaleLowerCase()===t)||null:null},vv=async()=>{const n=h();return n?av(await os(n.bookId)):null},ss=async(n,i,r,u,f)=>{const e=await lt(`/api/word-companion/scenes/${i}/companion`);try{e.revisionStatus=await vk(n,i)}catch{e.revisionStatus=""}ir=f||"Title Match";kt=!1;li(!1);hf("Linked to PlotDirector");t("");we();us();wr(i,r,u);ak(e);vg(i,r);ys();ps()},n=(n,t)=>{if(n){const i=t===null||t===undefined?"":String(t);n.textContent!==i&&(n.textContent=i)}},f=t=>{const i=n=>Object.prototype.hasOwnProperty.call(t,n);i("host")&&n(wb,t.host);i("platform")&&n(bb,t.platform);i("ready")&&n(kb,t.ready);i("wordApi")&&n(db,t.wordApi);i("lastScan")&&n(gb,t.lastScan);i("lastError")&&n(nk,t.lastError);i("paragraphs")&&n(fk,t.paragraphs);i("heading1")&&n(ek,t.heading1);i("heading2")&&n(ok,t.heading2)},o=n=>{const t=[];return n?.name&&t.push(n.name),n?.code&&n.code!==n.name&&t.push(n.code),n?.message&&t.push(n.message),n?.debugInfo?.errorLocation&&t.push(`Location: ${n.debugInfo.errorLocation}`),t.length>0?t.join(": "):String(n)},yv=async()=>{if(!window.Office||typeof window.Office.onReady!="function")return bt=!1,et=!1,lo="Unavailable",ao="Unavailable",f({host:lo,platform:ao,ready:"No",wordApi:"No"}),null;const n=await window.Office.onReady();return bt=!0,lo=n?.host||"Unknown",ao=n?.platform||"Unknown",et=!!window.Word&&!!window.Office.HostType&&n?.host===window.Office.HostType.Word,f({host:lo,platform:ao,ready:"Yes",wordApi:et?"Yes":"No"}),n},hs=n=>{try{const i=window.localStorage.getItem(n),t=Number.parseInt(i||"",10);return Number.isInteger(t)&&t>0?t:null}catch{return null}},v=(n,t)=>{try{t?window.localStorage.setItem(n,String(t)):window.localStorage.removeItem(n)}catch{}},c=(n,t)=>{if(n){n.innerHTML="";const i=document.createElement("option");i.value="";i.textContent=t;n.append(i);n.disabled=!0}},cs=(n,t,i,r,u)=>{if(n){n.innerHTML="";const f=document.createElement("option");f.value="";f.textContent=t;n.append(f);for(const t of i){const i=document.createElement("option");i.value=String(t[r]);i.textContent=t[u];n.append(i)}n.disabled=i.length===0}},br=n=>{const t=String(n?.displayTitle||"").trim();if(t)return t;const i=String(n?.title||"").trim(),r=String(n?.subtitle||"").trim();return r?`${i}: ${r}`:i},pk=()=>rr?.projectTitle||gt()?.title||"Project",wk=()=>rr?.bookTitle||br(h())||"Book",wc=()=>{const t=!!ii;r(lp,!t);r(kp,t);t&&(n(ap,pk()),n(vp,wk()))},gt=()=>{const n=Number.parseInt(p?.value||"",10);return tr.find(t=>t.projectId===n)||null},h=()=>{const n=Number.parseInt(ft?.value||"",10);return ti.find(t=>t.bookId===n)||null},kr=()=>{const n=Number.parseInt(ut?.value||"",10);return tr.find(t=>t.projectId===n)||null},lu=()=>{const n=Number.parseInt(d?.value||"",10);return ti.find(t=>t.bookId===n)||null},bk=()=>{const n=ii?.projectId||kr()?.projectId||gt()?.projectId||hs(s.projectId);return Number.isInteger(n)&&n>0?n:null},kk=()=>{const n=ii?.bookId||lu()?.bookId||h()?.bookId||hs(s.bookId);return Number.isInteger(n)&&n>0?n:null},dk=()=>{const n=String(window.Office?.context?.document?.url||"").trim();if(!n)return"";const t=n.split(/[\\/]/).filter(Boolean).pop()||"";try{return decodeURIComponent(t)}catch{return t}},pv=()=>({userID:Number.isInteger(oh)&&oh>0?oh:null,companionVersion:sk,machineName:"",documentOpen:!!et,currentDocumentName:dk(),linkedProjectID:bk(),linkedBookID:kk()}),gk=async()=>{hi&&hi.state===signalR.HubConnectionState.Connected&&await hi.invoke("CompanionHeartbeat",pv())},yf=()=>{gk().catch(n=>{console.debug("Word Companion heartbeat unavailable.",n)})},wv=async()=>{if(gu&&window.signalR&&!hi){hi=(new signalR.HubConnectionBuilder).withUrl("/hubs/word-companion-follow").withAutomaticReconnect().build();hi.onreconnected(yf);hi.onclose(()=>{fu&&(window.clearInterval(fu),fu=null)});await hi.start();await hi.invoke("RegisterCompanion",pv());fu=window.setInterval(yf,25e3)}};window.addEventListener("beforeunload",()=>{fu&&(window.clearInterval(fu),fu=null),hi&&hi.stop().catch(()=>{})});const ri=t=>n(wp,t),rt=()=>{const n=kr(),t=lu(),i=nf&&Date.now()-ee>=750;er&&(er.disabled=!n||!t||oe);uo&&(uo.disabled=!n||!String(bf?.value||"").trim()||oe);ds&&(ds.disabled=!yr?.canImport||!fe||oe);fo&&(fo.disabled=!i||ch||bv().length===0)},au=n=>{r(yp,!n);r(cp,n);lc();wc();for(const t of cl)r(t,n?!0:t.dataset.tabPanel!=="scene"&&!t.classList.contains("is-active"));n||ov()},bc=()=>{if(ut){if(tr.length===0){c(ut,"No projects available.");return}cs(ut,"Choose a project",tr,"projectId","title");const n=Number.parseInt(p?.value||"",10);Number.isInteger(n)&&n>0&&(ut.value=String(n))}},nd=(n=null)=>{if(d){if(ti.length===0){c(d,"No books available.");return}cs(d,"Choose a book",ti.map(n=>({...n,displayTitle:br(n)})),"bookId","displayTitle");const i=n||Number.parseInt(ft?.value||"",10),t=ti.find(n=>n.bookId===i);t&&(d.value=String(t.bookId));rt()}},ht=(t="Select a Project and Book to begin.")=>{yr=null,fe=null,si=[],wo=!1,nf=!1,ee=0,r(or,!0),r(ks,!0),r(yu,!0),r(wu,!0),or&&(or.innerHTML=""),bi&&(bi.innerHTML=""),n(pu,""),ri(t),rt()},bv=()=>bi?[...bi.querySelectorAll("input[type='checkbox']:checked")].map(n=>String(n.value||"").trim()).filter(Boolean):[],td=t=>{if(si=Array.isArray(t)?t:[],yu&&bi){if(bi.innerHTML="",r(yu,!1),r(wu,!wo||si.length===0),r(fo,!1),r(ll,!1),r(al,!0),si.length===0){n(pu,"No likely major characters found.");rt();return}n(pu,`${si.length} likely major characters found.`);for(const n of si){const i=document.createElement("label");i.className="word-companion-character-candidate";const t=document.createElement("input");t.type="checkbox";t.value=n.text||"";t.checked=String(n.confidence||"").trim().toLowerCase()==="high";t.addEventListener("change",rt);const u=document.createElement("strong");u.textContent=n.text||"";const f=document.createElement("em"),e=String(n.reason||"").trim();f.textContent=e?`${Number(n.mentionCount||0).toLocaleString()} mentions · ${e}`:`${Number(n.mentionCount||0).toLocaleString()} mentions`;const r=document.createElement("span");r.className="word-companion-character-candidate-text";r.append(u,f);i.append(t,r);bi.append(i)}rt()}},id=n=>{if(or){or.innerHTML="";const t=document.createElement("strong");t.textContent="Manuscript Analysis";or.append(t);const i=document.createElement("dl"),u=[["Project",n.projectTitle],["Book",br({title:n.bookTitle,subtitle:n.bookSubtitle})],["Chapters",Number(n.chapterCount||0).toLocaleString()],["Scenes",Number(n.sceneCount||0).toLocaleString()],["Words",Number(n.wordCount||0).toLocaleString()]];for(const[n,t]of u){const r=document.createElement("dt");r.textContent=n;const u=document.createElement("dd");u.textContent=t||"-";i.append(r,u)}or.append(i);r(or,!1);r(ks,!1);ri(n.message||"Preview generated.");rt()}},kc=()=>{const n=window.Office?.context?.document?.settings;if(!n)return null;const u=String(n.get(wi.documentGuid)||"").trim(),t=Number.parseInt(n.get(wi.bookId)||"",10),i=Number.parseInt(n.get(wi.projectId)||"",10),r=Number.parseInt(n.get(wi.bindingVersion)||"",10);return!u||!Number.isInteger(t)||t<=0||!Number.isInteger(i)||i<=0?null:{documentGuid:u,bookId:t,projectId:i,bindingVersion:Number.isInteger(r)&&r>0?r:1}},rd=()=>{const i=ii||kc();if(i)return i;const r=rr?.manuscriptDocument?.documentGuid||"",n=Number.parseInt(rr?.bookId||"",10),t=Number.parseInt(rr?.projectId||"",10);return r&&Number.isInteger(n)&&n>0&&Number.isInteger(t)&&t>0?{documentGuid:r,bookId:n,projectId:t,bindingVersion:Number.parseInt(rr?.manuscriptDocument?.bindingVersion||"",10)||1}:null},ud=n=>new Promise((t,i)=>{const r=window.Office?.context?.document?.settings;if(!r){i(new Error("Word document settings are unavailable."));return}r.set(wi.documentGuid,n.documentGuid);r.set(wi.bookId,String(n.bookId));r.set(wi.projectId,String(n.projectId));r.set(wi.bindingVersion,String(n.bindingVersion||1));r.saveAsync(n=>{n.status===window.Office.AsyncResultStatus.Succeeded?t():i(n.error||new Error("Unable to save Word document metadata."))})}),kv=()=>new Promise((n,t)=>{const i=window.Office?.context?.document?.settings;if(!i){t(new Error("Word document settings are unavailable."));return}for(const n of Object.values(wi))typeof i.remove=="function"?i.remove(n):i.set(n,"");i.saveAsync(i=>{i.status===window.Office.AsyncResultStatus.Succeeded?n():t(i.error||new Error("Unable to remove Word document metadata."))})}),dv=()=>ii?.documentGuid?ii.documentGuid:window.crypto?.randomUUID?window.crypto.randomUUID():"10000000-1000-4000-8000-100000000000".replace(/[018]/g,n=>(Number(n)^Math.random()*16>>Number(n)/4).toString(16)),dr=()=>ii?.documentGuid||kc()?.documentGuid||"",pi=()=>{const t=gt(),n=h();gl&&(gl.textContent=t?.title||"(Not connected)");na&&(na.textContent=n?br(n):"(Not connected)");sa&&(sa.textContent=n?"":"Select a PlotDirector book before refreshing.");ts();wc()},gv=()=>{const t=Array.isArray(st.characters)?st.characters.length:0,i=Array.isArray(st.assets)?st.assets.length:0,r=Array.isArray(st.locations)?st.locations.length:0;n(fw,`Characters (${t})`);n(ew,`Assets (${i})`);n(ow,`Locations (${r})`)},ke=n=>{const t=n?.styleBuiltIn||n?.style||"";return String(t).replace(/\s+/g,"").toLowerCase()},pf=(n,t)=>{const i=ke(n);return i===`heading${t}`||i.endsWith(`.heading${t}`)||i.includes(`heading${t}`)},de=n=>{const t=String(n||"").trim().split(/\s+/).filter(Boolean);return t.length},fd=(n,t)=>String(n?.text||"").trim()===String(t?.text||"").trim()&&ke(n)===ke(t),ny=(n,t)=>{const r=String(n||"").match(new RegExp(`^${t}-(\\d+)$`,"i"));if(!r)return null;const i=Number.parseInt(r[1],10);return Number.isInteger(i)&&i>0?i:null},ty=n=>{try{return Array.isArray(n?.contentControls?.items)?n.contentControls.items:[]}catch(t){return console.warn("Word paragraph content controls were not available.",t),[]}},iy=(n,t)=>{const i=ty(n);for(const n of i){const i=ny(n.tag,t);if(i)return i}return null},ed=(n,t)=>{const u=[];let o=0,s=0,i=null,r=null;n.forEach((n,t)=>{const f=String(n.text||"").trim();if(f){if(pf(n,1)){o+=1;i={index:u.length+1,paragraphIndex:t,title:f,anchorId:iy(n,"PD-CHAPTER"),scenes:[]};u.push(i);r=null;return}if(pf(n,2)){if(s+=1,!i)return;r={index:i.scenes.length+1,paragraphIndex:t,title:f,anchorId:iy(n,"PD-SCENE"),wordCount:0};i.scenes.push(r);return}r&&(r.wordCount+=de(f))}});const h=t.find(n=>String(n.text||"").trim()),f=h?n.findIndex(n=>fd(n,h)):-1,e=f>=0?u.filter(n=>n.paragraphIndex<=f).at(-1)||null:null,c=e&&f>=0?e.scenes.filter(n=>n.paragraphIndex<=f).at(-1)||null:null;return{chapters:u,currentChapter:e,currentScene:c,paragraphCount:n.length,heading1Count:o,heading2Count:s}},od=(n,t)=>{const r=[];let i=null,u=null,f=0;const e=[],s=new Set(["***","###"]),o=()=>{if(!i)return null;const n={title:`Scene ${i.scenes.length+1}`,sortOrder:(i.scenes.length+1)*10,wordCount:0};return i.scenes.push(n),n};return n.forEach(n=>{const h=String(n.text||"").trim();if(h){if(e.push(h),pf(n,1)){i={title:h,sortOrder:(r.length+1)*10,wordCount:0,scenes:[]};r.push(i);u=o();f+=de(h);return}if(i&&u){if(t&&s.has(h)){u=o();return}const c=de(h);i.wordCount+=c;u.wordCount+=c;f+=c}}}),{chapters:r,chapterCount:r.length,sceneCount:r.reduce((n,t)=>n+t.scenes.length,0),wordCount:f,documentText:e.join("\n")}},ry=new Set(["***","###"]),sd=(n,t)=>{const i=String(n?.text||"").trim();return{index:t,text:i,styleBuiltIn:n?.styleBuiltIn||"",style:n?.style||"",wordCount:de(i),chapterAnchorId:null,sceneAnchorId:null}},uy=(n,t)=>{n&&(n.endParagraphIndex=t)},fy=(n,t)=>{n&&(n.endParagraphIndex=t,uy(n.scenes.at(-1),t))},hd=(n,t)=>{const u=n.map(sd),f=[];let o=0,s=0,i=null,r=null,e=0;const h=(n,t=null,u=null)=>i?(uy(r,n.index-1),r={index:i.scenes.length+1,paragraphIndex:n.index,startParagraphIndex:n.index,endParagraphIndex:n.index,title:t||`Scene ${i.scenes.length+1}`,anchorId:u,wordCount:0},i.scenes.push(r),r):null;for(const n of u)if(n.text){if(pf(n,1)){fy(i,n.index-1);o+=1;i={index:f.length+1,paragraphIndex:n.index,startParagraphIndex:n.index,endParagraphIndex:n.index,title:n.text,anchorId:n.chapterAnchorId,wordCount:0,scenes:[]};f.push(i);r=null;h(n,t?"Scene 1":"Scene 1",n.sceneAnchorId);e+=n.wordCount;continue}if(i){if(t&&ry.has(n.text)){s+=1;h(n);continue}i.wordCount+=n.wordCount;r&&(r.wordCount+=n.wordCount);e+=n.wordCount}}return fy(i,u.length-1),{paragraphs:u,chapters:f,usesExplicitScenes:!!t,paragraphCount:u.length,heading1Count:o,heading2Count:s,documentWordCount:e,currentParagraphIndex:null,currentChapter:null,currentScene:null}},cd=(n,t)=>String(n?.text||"").trim()===String(t?.text||"").trim()&&ke(n)===ke(t),ey=(n,t)=>{const r=(t||[]).find(n=>String(n.text||"").trim());if(!n||!r)return n?.currentParagraphIndex??-1;const i=n.paragraphs.filter(n=>cd(n,r)).map(n=>n.index);return i.length===0?n.currentParagraphIndex??-1:Number.isInteger(n.currentParagraphIndex)?i.reduce((t,i)=>Math.abs(i-n.currentParagraphIndex)!n||!Number.isInteger(t)||t<0?null:n.chapters.filter(n=>n.startParagraphIndex<=t).at(-1)||null,ad=(n,t)=>!n||!Number.isInteger(t)||t<0?null:n.scenes.filter(n=>n.startParagraphIndex<=t).at(-1)||null,oy=n=>{if(!l)return!1;const t=ld(l,n),i=ad(t,n),r=t?.startParagraphIndex!==l.currentChapter?.startParagraphIndex||i?.startParagraphIndex!==l.currentScene?.startParagraphIndex||t?.anchorId!==l.currentChapter?.anchorId||i?.anchorId!==l.currentScene?.anchorId;return(l.currentParagraphIndex=n,l.currentChapter=t,l.currentScene=i,!r)?!1:(cf(t?.title,i?.title,i?.wordCount,t?.anchorId,i?.anchorId,t?.index),r)},dc=n=>{if(kf){if(kf.innerHTML="",n.chapters.length===0){const n=document.createElement("p");n.textContent="No chapters detected. Use Heading 1 for chapter titles.";kf.append(n);return}const t=n.chapters.some(n=>n.scenes.length>0);if(!t){const n=document.createElement("p");n.textContent="No scenes detected. Use Heading 2 for scene titles.";kf.append(n)}for(const t of n.chapters){const n=document.createElement("div");n.className="word-companion-outline-chapter";const i=document.createElement("strong");if(i.textContent=t.title,n.append(i),t.scenes.length>0){const i=document.createElement("ul");for(const n of t.scenes){const t=document.createElement("li");t.textContent=n.title;i.append(t)}n.append(i)}kf.append(n)}}},sy=[{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"}],vd={alreadyLinked:"Already Linked",couldLink:"Could Link",wouldCreateChapter:"Would Create Chapter",wouldCreateScene:"Would Create Scene",manualReview:"Manual Review Required"},yd={alreadyLinked:"✓",couldLink:"~",wouldCreateChapter:"+",wouldCreateScene:"+",manualReview:"?"},ls=(n,t)=>it(n,t).toLocaleLowerCase(),ui=({category:i,action:n,type:s,wordTitle:l,match:r="",anchorStatus:t="None",matches:u=[],pdChapter:e=null,pdScene:o=null,wordChapter:h=null,wordScene:c=null,parentItem:f=null})=>({id:`${s}-${h?.index||0}-${c?.index||0}-${h?.paragraphIndex??c?.paragraphIndex??0}`,category:i,action:n,type:s,wordTitle:l,match:r,anchorStatus:t,matches:u,pdChapter:e,pdScene:o,wordChapter:h,wordScene:c,parentItem:f,result:"Pending",error:""}),fi=(n,t)=>{Array.isArray(n[t.category])&&n[t.category].push(t)},gc=n=>Array.isArray(n?.scenes)?n.scenes:[],pd=(n,t)=>{for(const i of n){const n=gc(i).find(n=>n.sceneId===t);if(n)return{chapter:i,scene:n}}return null},wd=(n,t,i)=>{const u=n.anchorId?`PD-CHAPTER-${n.anchorId}`:"None";if(n.anchorId){const r=t.find(t=>t.chapterId===n.anchorId);if(r){const t=ui({category:"alreadyLinked",action:"alreadyLinked",type:"Chapter",wordTitle:n.title,match:`Chapter ${r.chapterId}: ${r.title}`,anchorStatus:u,pdChapter:r,wordChapter:n});return fi(i,t),t}const f=ui({category:"manualReview",action:"manualReview",type:"Chapter",wordTitle:n.title,anchorStatus:`${u} not found`,matches:[],pdChapter:null,wordChapter:n});return fi(i,f),f}const r=t.filter(t=>ls(t.title,"chapter")===ls(n.title,"chapter"));if(r.length===1){const t=ui({category:"couldLink",action:"couldLink",type:"Chapter",wordTitle:n.title,match:`Chapter ${r[0].chapterId}: ${r[0].title}`,anchorStatus:u,pdChapter:r[0],wordChapter:n});return fi(i,t),t}if(r.length>1){const t=ui({category:"manualReview",action:"manualReview",type:"Chapter",wordTitle:n.title,anchorStatus:u,matches:r.map(n=>`Chapter ${n.chapterId}: ${n.title}`),pdChapter:null,wordChapter:n});return fi(i,t),t}const f=ui({category:"wouldCreateChapters",action:"wouldCreateChapter",type:"Chapter",wordTitle:n.title,anchorStatus:u,pdChapter:null,wordChapter:n});return fi(i,f),f},bd=(n,t,i,r)=>{const u=n.anchorId?`PD-SCENE-${n.anchorId}`:"None";if(n.anchorId){const f=pd(i,n.anchorId);if(f){fi(r,ui({category:"alreadyLinked",action:"alreadyLinked",type:"Scene",wordTitle:n.title,match:`Scene ${f.scene.sceneId}: ${f.scene.title}`,anchorStatus:u,pdChapter:f.chapter,pdScene:f.scene,wordChapter:t?.wordChapter,wordScene:n,parentItem:t}));return}fi(r,ui({category:"manualReview",action:"manualReview",type:"Scene",wordTitle:n.title,anchorStatus:`${u} not found`,wordChapter:t?.wordChapter,wordScene:n,parentItem:t}));return}if(!t?.pdChapter&&t?.category==="manualReview"){fi(r,ui({category:"manualReview",action:"manualReview",type:"Scene",wordTitle:n.title,anchorStatus:u,matches:["Parent chapter requires manual review."],wordChapter:t?.wordChapter,wordScene:n,parentItem:t}));return}if(!t?.pdChapter){fi(r,ui({category:"wouldCreateScenes",action:"wouldCreateScene",type:"Scene",wordTitle:n.title,anchorStatus:u,wordChapter:t?.wordChapter,wordScene:n,parentItem:t}));return}const f=gc(t.pdChapter).filter(t=>ls(t.title,"scene")===ls(n.title,"scene"));if(f.length===1){fi(r,ui({category:"couldLink",action:"couldLink",type:"Scene",wordTitle:n.title,match:`Scene ${f[0].sceneId}: ${f[0].title}`,anchorStatus:u,pdChapter:t.pdChapter,pdScene:f[0],wordChapter:t.wordChapter,wordScene:n,parentItem:t}));return}if(f.length>1){fi(r,ui({category:"manualReview",action:"manualReview",type:"Scene",wordTitle:n.title,anchorStatus:u,matches:f.map(n=>`Scene ${n.sceneId}: ${n.title}`),wordChapter:t.wordChapter,wordScene:n,parentItem:t}));return}fi(r,ui({category:"wouldCreateScenes",action:"wouldCreateScene",type:"Scene",wordTitle:n.title,anchorStatus:u,pdChapter:t.pdChapter,wordChapter:t.wordChapter,wordScene:n,parentItem:t}))},kd=(n,t)=>{const i={alreadyLinked:[],couldLink:[],wouldCreateChapters:[],wouldCreateScenes:[],manualReview:[]},r=Array.isArray(t?.chapters)?t.chapters:[];for(const t of Array.isArray(n?.chapters)?n.chapters:[]){const n=wd(t,r,i);for(const u of gc(t))bd(u,n,r,i)}return i},dd=n=>{const t=document.createElement("article");t.className="word-companion-preview-item";const i=document.createElement("strong");i.textContent=`${yd[n.action]||""} ${n.type}: ${n.wordTitle}`;t.append(i);const r=document.createElement("span");if(r.textContent=`Anchor: ${n.anchorStatus||"None"}`,t.append(r),n.match){const i=document.createElement("span");i.textContent=`Match: ${n.match}`;t.append(i)}if(Array.isArray(n.matches)&&n.matches.length>0){const i=document.createElement("div");i.className="word-companion-preview-matches";const r=document.createElement("span");r.textContent="Matches:";i.append(r);const u=document.createElement("ul");for(const t of n.matches){const n=document.createElement("li");n.textContent=t;u.append(n)}i.append(u);t.append(i)}const u=document.createElement("span");if(u.textContent=`Action: ${vd[n.action]||n.action}`,t.append(u),n.result&&n.result!=="Pending"){const i=document.createElement("span");i.textContent=n.error?`Result: ${n.result} - ${n.error}`:`Result: ${n.result}`;t.append(i)}return t},hy=n=>{if(du){du.innerHTML="";for(const t of sy){const i=document.createElement("section");i.className="word-companion-preview-group";const r=document.createElement("strong");r.textContent=t.title;i.append(r);const u=Array.isArray(n?.[t.key])?n[t.key]:[];if(u.length===0){const n=document.createElement("p");n.className="word-companion-empty-list";n.textContent="None";i.append(n)}else for(const n of u)i.append(dd(n));du.append(i)}ns()}},wf=async n=>{const t=n.document.body.paragraphs,i=n.document.getSelection().paragraphs;t.load("text,styleBuiltIn,style");i.load("text,styleBuiltIn,style");await n.sync();try{for(const n of t.items)(pf(n,1)||pf(n,2))&&n.contentControls.load("items/tag,title");await n.sync()}catch(u){console.warn("Unable to load Word heading content controls. Continuing without PlotDirector anchors.",u)}const r=ed(t.items,i.items);return r.bodyParagraphs=t.items,r},cy=async(n,t)=>{const u=n.document.body.paragraphs,f=n.document.getSelection().paragraphs;u.load("text,styleBuiltIn,style");f.load("text,styleBuiltIn,style");await n.sync();const r=hd(u.items,t),e=ey(r,f.items);return l=r,oy(e),i("Cache rebuilt."),r},htt=async n=>{const t=n.document.getSelection().paragraphs;return t.load("text,styleBuiltIn,style"),await n.sync(),t.items},y=()=>window.performance&&typeof window.performance.now=="function"?window.performance.now():Date.now(),ni=n=>!!n&&!n.stale&&n.generation===he&&n.documentGuid===dr()&&n.sceneId===u,ly=async(n={})=>{const r=!!n.includeSelection,w=n.updateDisplay!==!1,f=he,b=y(),k=gt(),d=h(),o=l?.currentScene;if(!o||!Number.isInteger(o.startParagraphIndex)||!Number.isInteger(o.endParagraphIndex)||!bt||!et||!window.Word||typeof window.Word.run!="function")return null;const g=y(),s=await window.Word.run(async n=>{const i=n.document.body.paragraphs;i.load("text");let t=null;return r&&(t=n.document.getSelection().paragraphs,t.load("text,styleBuiltIn,style")),await n.sync(),{bodyTexts:i.items.map(n=>String(n.text||"")),selectionParagraphs:r&&t?t.items.map(n=>({text:String(n.text||""),styleBuiltIn:n.styleBuiltIn,style:n.style})):[]}}),c=Math.round(y()-g);if(f!==he)return ci(`Office snapshot: ${c}ms stale`),{stale:!0,generation:f};let a=!1;if(r){const n=ey(l,s.selectionParagraphs);a=oy(n)}const t=l?.currentScene;if(!t||!Number.isInteger(t.startParagraphIndex)||!Number.isInteger(t.endParagraphIndex))return null;const v=[];let i=0;const nt=Math.min(t.endParagraphIndex,s.bodyTexts.length-1),tt=l.currentChapter?.startParagraphIndex;for(let n=t.startParagraphIndex;n<=nt;n+=1){const t=String(s.bodyTexts[n]||"").trim();t&&!ry.has(t)&&n!==tt&&(v.push(t),i+=de(t))}t.wordCount=i;w&&cf(l.currentChapter?.title,t.title,i,l.currentChapter?.anchorId,t.anchorId,l.currentChapter?.index);const p={documentGuid:dr(),projectId:k?.projectId||null,bookId:d?.bookId||null,chapterId:e,sceneId:u,chapterTitle:l.currentChapter?.title||"",sceneTitle:t.title||"",sceneText:v.join("\n"),wordCount:i,selectionSignature:`${l.currentParagraphIndex??-1}:${t.startParagraphIndex}:${t.endParagraphIndex}`,selectionChanged:a,generation:f};return ce=p,ci(`Office snapshot: ${c}ms; scene words: ${i}; total snapshot: ${Math.round(y()-b)}ms`),p},ay=async()=>ni(ce)?ce:await ly(),vy=async()=>{if(t(""),!bt||!et||!window.Word||typeof window.Word.run!="function")return cf(null,null,null),t("Word document access is unavailable."),f({lastScan:"Failure",lastError:"Word document access is unavailable."}),!1;gr&&(gr.disabled=!0,gr.textContent="Scanning...");t("Scanning document structure...");try{const n=await window.Word.run(async n=>{const t=h();return t?await cy(n,!!t.usesExplicitScenes):(l=null,await wf(n))});return l||cf(n.currentChapter?.title,n.currentScene?.title,n.currentScene?.wordCount,n.currentChapter?.anchorId,n.currentScene?.anchorId,n.currentChapter?.index),nt("Not detected"),dc(n),t(""),f({lastScan:"Success",lastError:"-",paragraphs:String(n.paragraphCount),heading1:String(n.heading1Count),heading2:String(n.heading2Count)}),!0}catch(n){const i=o(n);return console.error("Unable to scan the Word document.",n),cf(null,null,null),t(`Unable to scan the Word document: ${i}`),f({lastScan:"Failure",lastError:i}),!1}finally{gr&&(gr.disabled=!1,gr.textContent="Refresh Document Structure")}},yy=async()=>{const n=h();if(!gt()||!n)return fr("Select a project and book to analyse manuscript structure."),!1;if(!bt||!et||!window.Word||typeof window.Word.run!="function")return fr("Word document access is unavailable."),f({lastScan:"Failure",lastError:"Word document access is unavailable."}),!1;sr&&(sr.disabled=!0,sr.textContent="Analysing...");fr("Analysing manuscript structure...");try{const t=await window.Word.run(async n=>await wf(n)),i=await lt(`/api/word-companion/books/${n.bookId}/structure`);return cf(t.currentChapter?.title,t.currentScene?.title,t.currentScene?.wordCount,t.currentChapter?.anchorId,t.currentScene?.anchorId,t.currentChapter?.index),dc(t),vi=kd(t,i),hy(vi),fr("Preview generated. No changes have been applied."),f({lastScan:"Success",lastError:"-",paragraphs:String(t.paragraphCount),heading1:String(t.heading1Count),heading2:String(t.heading2Count)}),!0}catch(t){return console.error("Unable to analyse manuscript structure.",t),fr("Unable to analyse manuscript structure."),f({lastScan:"Failure",lastError:o(t)}),!1}finally{sr&&(sr.textContent="Analyse Manuscript Structure",ts())}},gd=(n=vi)=>{const t=eu(n),i=t.filter(n=>n.category==="couldLink"&&n.type==="Chapter").length,r=t.filter(n=>n.category==="couldLink"&&n.type==="Scene").length,u=t.filter(n=>n.category==="wouldCreateChapters").length,f=t.filter(n=>n.category==="wouldCreateScenes").length,e=Array.isArray(n?.manualReview)?n.manualReview.length:0;return{linkChapters:i,linkScenes:r,createChapters:u,createScenes:f,manualReview:e}},ng=n=>{if(eh){eh.innerHTML="";const i=[`Link ${n.linkChapters+n.linkScenes} chapters/scenes`,`Create ${n.createChapters} chapters`,`Create ${n.createScenes} scenes`],t=document.createElement("ul");for(const n of i){const i=document.createElement("li");i.textContent=n;t.append(i)}eh.append(t)}},nl=n=>{bo&&(bo(n),bo=null),nr?.close?nr.close():nr&&(nr.hidden=!0)},tg=()=>{const n=gd();return(ng(n),!nr)?Promise.resolve(window.confirm(`Apply Structure Sync? + +This will: + +- Link ${n.linkChapters+n.linkScenes} chapters/scenes +- Create ${n.createChapters} chapters +- Create ${n.createScenes} scenes + +Manual review items will be skipped.`)):new Promise(n=>{bo=n,nr.showModal?nr.showModal():nr.hidden=!1})},ge=(n,t,i="")=>{n.result=t,n.error=i},py=n=>Number.isInteger(n?.wordChapter?.index)&&n.wordChapter.index>0?n.wordChapter.index*10:null,wy=n=>Number.isInteger(n?.wordScene?.index)&&n.wordScene.index>0?n.wordScene.index*10:null,ig=async(n,t)=>{const i=await ct(`/api/word-companion/books/${n}/chapters`,{title:cu(t.wordTitle),sortOrder:py(t)});if(!i?.chapterId)throw new Error("Chapter creation did not return a chapter ID.");ou();t.pdChapter={chapterId:i.chapterId,title:i.title||t.wordTitle,sortOrder:i.sortOrder||py(t)||0,scenes:[]};t.match=`Chapter ${t.pdChapter.chapterId}: ${t.pdChapter.title}`},rg=async(n,t)=>{const i=t.pdChapter||t.parentItem?.pdChapter;if(!i?.chapterId)throw new Error("Scene parent chapter is not available.");const r=await ct(`/api/word-companion/books/${n}/chapters/${i.chapterId}/scenes`,{sceneTitle:cu(t.wordTitle),sortOrder:wy(t)});if(!r?.sceneId)throw new Error("Scene creation did not return a scene ID.");ou();t.pdChapter=i;t.pdScene={sceneId:r.sceneId,title:t.wordTitle,sortOrder:wy(t)||0};t.match=`Scene ${t.pdScene.sceneId}: ${t.pdScene.title}`},ug=async n=>{if(!bt||!et||!window.Word||typeof window.Word.run!="function")throw new Error("Word document access is unavailable.");await window.Word.run(async t=>{const u=await wf(t),r=n.type==="Chapter"?n.wordChapter:n.wordScene,i=Number.isInteger(r?.paragraphIndex)?u.bodyParagraphs[r.paragraphIndex]:null;if(!i)throw new Error("Unable to locate the Word heading.");if(n.type==="Chapter"){const t=n.pdChapter?.chapterId;if(!t)throw new Error("Chapter ID is not available.");no(i,"PlotDirector Chapter",`PD-CHAPTER-${t}`,"PD-CHAPTER")}else{const t=n.pdScene?.sceneId;if(!t)throw new Error("Scene ID is not available.");no(i,"PlotDirector Scene",`PD-SCENE-${t}`,"PD-SCENE")}await t.sync()})},fg=n=>`Structure Sync Complete. Created: ${n.createdChapters} Chapters, ${n.createdScenes} Scenes. Linked: ${n.linkedChapters} Chapters, ${n.linkedScenes} Scenes. Skipped: ${n.skipped} Manual Review items. Failed: ${n.failed} Items.`,eg=async()=>{const r=h();if(!r||eu().length===0||se){ns();return}const f=await tg();if(f){se=!0;ts();fr("Applying structure sync...");const n={createdChapters:0,createdScenes:0,linkedChapters:0,linkedScenes:0,skipped:Array.isArray(vi?.manualReview)?vi.manualReview.length:0,failed:0},e=eu().filter(n=>n.category==="couldLink"&&n.type==="Chapter"),s=eu().filter(n=>n.category==="wouldCreateChapters"),c=eu().filter(n=>n.category==="couldLink"&&n.type==="Scene"),l=eu().filter(n=>n.category==="wouldCreateScenes"),u=[];let t="";const i=(n,t)=>{u.push({item:n,counterName:t})},a=async(t,i)=>{try{await ug(t);ge(t,"Success");n[i]+=1}catch(r){ge(t,"Failed",o(r));n.failed+=1}};try{for(const n of e)i(n,"linkedChapters");for(const t of s)try{await ig(r.bookId,t);i(t,"createdChapters")}catch(u){ge(t,"Failed",o(u));n.failed+=1}for(const n of c)i(n,"linkedScenes");for(const t of l)try{await rg(r.bookId,t);i(t,"createdScenes")}catch(u){ge(t,"Failed",o(u));n.failed+=1}for(const n of u)await a(n.item,n.counterName);for(const n of vi.manualReview||[])ge(n,"Skipped");hy(vi);t=fg(n);fr(t)}finally{se=!1;ts();t&&(ou(),await yy(),fr(`${t} Preview refreshed.`))}}},og=async()=>{tu&&(tu.disabled=!0,tu.textContent="Running...");try{if(await yv(),!bt||!et||!window.Word||typeof window.Word.run!="function"){f({lastScan:"Failure",lastError:"Word document access is unavailable.",paragraphs:"-",heading1:"-",heading2:"-"});return}const n=await window.Word.run(async n=>{const t=n.document.body.paragraphs;return t.load("items"),await n.sync(),t.items.length});f({lastScan:"Success",lastError:"-",paragraphs:String(n)})}catch(n){console.error("Word Companion diagnostics failed.",n);const i=o(n);f({lastScan:"Failure",lastError:i});t(`Unable to scan the Word document: ${i}`)}finally{tu&&(tu.disabled=!1,tu.textContent="Run Diagnostics")}},lt=async(n,t={})=>{const i=await window.fetch(n,{credentials:"same-origin",...t,headers:{Accept:"application/json",...(t.headers||{})}});if(!i.ok)throw new Error(`Request failed: ${i.status}`);return await i.json()},ct=(n,t)=>lt(n,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)}),sg=async()=>{const n=kr(),t=lu();if(!n||!t){ht("Select a Project and Book to begin.");return}if(!bt||!et||!window.Word||typeof window.Word.run!="function"){ht("Word document access is unavailable.");return}er&&(er.disabled=!0,er.textContent="Scanning...");ri("Scanning manuscript structure...");try{const i=await window.Word.run(async n=>{const i=n.document.body.paragraphs;return i.load("text,styleBuiltIn,style"),await n.sync(),od(i.items,!!t.usesExplicitScenes)});fe=i;wo=!1;const u=await ct("/api/word-companion/manuscript/analyse",{projectId:n.projectId,bookId:t.bookId,documentGuid:dv(),chapterCount:i.chapterCount,sceneCount:i.sceneCount,wordCount:i.wordCount});yr=u;id(u);try{const t=await ct("/api/word-companion/manuscript/discover-characters",{projectId:n.projectId,documentText:i.documentText||""});si=Array.isArray(t?.candidates)?t.candidates:[];r(yu,!0);r(wu,!0)}catch(e){console.error("Unable to discover character candidates.",e);si=[];r(yu,!0);r(wu,!0)}f({lastScan:"Success",lastError:"-",paragraphs:"-",heading1:String(i.chapterCount),heading2:"-"})}catch(i){console.error("Unable to scan manuscript for first-run import.",i);ht(`Unable to scan manuscript: ${o(i)}`);f({lastScan:"Failure",lastError:o(i)})}finally{er&&(er.textContent="Scan Manuscript");rt()}},hg=()=>new Promise(n=>{if(!bu||typeof bu.showModal!="function"){n(window.confirm("This Book already contains planned chapters and scenes.\n\nImporting this manuscript will replace the planned structure.\n\nContinue?"));return}const t=t=>{vl?.removeEventListener("click",i),yl?.removeEventListener("click",r),bu?.removeEventListener("cancel",u),bu?.close(),n(t)},i=()=>t(!0),r=()=>t(!1),u=n=>{n.preventDefault(),t(!1)};vl?.addEventListener("click",i);yl?.addEventListener("click",r);bu?.addEventListener("cancel",u);bu.showModal()}),cg=()=>new Promise(n=>{if(!ku||typeof ku.showModal!="function"){n(window.confirm("This will remove PlotDirector binding metadata from this Word document.\n\nIf PlotDirector is reachable, it will also unlink this Book in PlotDirector.\n\nYour manuscript text will not be changed.\n\nContinue?"));return}const t=t=>{pl?.removeEventListener("click",i),wl?.removeEventListener("click",r),ku?.removeEventListener("cancel",u),ku?.close(),n(t)},i=()=>t(!0),r=()=>t(!1),u=n=>{n.preventDefault(),t(!1)};pl?.addEventListener("click",i);wl?.addEventListener("click",r);ku?.addEventListener("cancel",u);ku.showModal()}),lg=async()=>{const i=kr(),u=lu();if(!i||!u||!fe||!yr?.canImport){rt();return}let n=!1;if(yr.requiresReplaceConfirmation&&(n=await hg(),!n)){ri("Import cancelled.");return}oe=!0;rt();ri("Importing manuscript structure...");try{const h=dv(),f=await ct("/api/word-companion/manuscript/import",{projectId:i.projectId,bookId:u.bookId,documentGuid:h,bindingVersion:1,replacePlanned:n,chapters:fe.chapters});if(!f.imported){yr={...yr,canImport:!f.requiresReplaceConfirmation,requiresReplaceConfirmation:!!f.requiresReplaceConfirmation,message:f.message||yr.message};ri(f.message||"Import was not completed.");rt();return}const e=f.manuscriptDocument;await ud({documentGuid:e?.documentGuid||h,bookId:f.bookId,projectId:f.projectId,bindingVersion:e?.bindingVersion||1});ii={documentGuid:e?.documentGuid||h,bookId:f.bookId,projectId:f.projectId,bindingVersion:e?.bindingVersion||1};v(s.projectId,f.projectId);v(s.bookId,f.bookId);p&&(p.value=String(f.projectId));const o=si;await vu(f.projectId,f.bookId);pe("Connected");wo=!0;nf=o.length>0;ee=nf?Date.now():0;si=o;o.length>0?(au(!0),td(o),r(ks,!0),r(wu,!1),ri(f.message||"Manuscript structure imported."),t(f.message||"Manuscript structure imported."),window.setTimeout(rt,750)):await as(f.message||"Manuscript structure imported.")}catch(f){console.error("Unable to import manuscript structure.",f);ri(`Unable to import manuscript: ${o(f)}`)}finally{oe=!1;rt()}},as=async(n="")=>{nf=!1;ee=0;si=[];bi&&(bi.innerHTML="");r(yu,!0);au(!1);ri("");r(wu,!0);n&&t(n);const i=await by();n&&i&&w&&t(n)},ag=async()=>{const i=kr()||gt(),r=bv();if(!nf||Date.now()-ee<750||!i||r.length===0){rt();return}ch=!0;rt();n(pu,"Creating selected characters...");try{const u=await ct("/api/word-companion/manuscript/create-discovered-characters",{projectId:i.projectId,candidateNames:r});t(u.message||"Characters created.");n(pu,`${u.createdCount||0} created, ${u.skippedCount||0} already existed.`);await as(u.message||"Characters created.")}catch(u){console.error("Unable to create discovered characters.",u);n(pu,`Unable to create characters: ${o(u)}`)}finally{ch=!1;rt()}},vg=async(n=u,t=e)=>{const f=gt(),o=h(),c=dr(),r=Number.parseInt(n||"",10),s=Number.parseInt(t||"",10);if(f&&o&&c&&Number.isInteger(r)&&!(r<=0)&&Number.isInteger(s)&&!(s<=0)){const l=`${f.projectId}:${o.bookId}:${r}`;if(ec!==l){ec=l;try{await ct("/api/word-companion/runtime/current-scene",{documentGuid:c,projectId:f.projectId,bookId:o.bookId,chapterId:s,sceneId:r});i("Current scene follow event sent.")}catch(a){ec="";console.debug("Unable to notify PlotDirector of current scene.",a)}}}},by=async()=>{const n=h();if(!n||!bt||!et||!window.Word||typeof window.Word.run!="function")return!1;t("Reading manuscript position...");try{ou();await Promise.all([rl(!0),ul(!0),fl(!0)]);await os(n.bookId);const i=await window.Word.run(async t=>await cy(t,!!n.usesExplicitScenes));return(dc(i),f({lastScan:"Success",lastError:"-",paragraphs:String(i.paragraphCount),heading1:String(i.heading1Count),heading2:String(i.heading2Count)}),!w)?(nt("Not detected"),li(!1),k("Live"),t("Manuscript linked. Click inside a chapter to show the current scene."),!0):(tt?await ro():await ol(),li(!1),k("Live"),t(""),!0)}catch(i){return console.error("Unable to initialise bound manuscript runtime.",i),l=null,k("Manual"),t(`Unable to read manuscript position: ${o(i)}`),f({lastScan:"Failure",lastError:o(i)}),!1}},yg=async()=>{if(!gu||!et){au(!1);return}const n=kc();if(n)try{const t=await lt(`/api/word-companion/runtime/book/${n.bookId}?documentGuid=${encodeURIComponent(n.documentGuid)}`),i=t?.manuscriptDocument;if(t?.bookId===n.bookId&&t?.projectId===n.projectId&&i&&String(i.documentGuid).toLowerCase()===n.documentGuid.toLowerCase()){ii=n;hv(t);v(s.projectId,t.projectId);v(s.bookId,t.bookId);p&&(p.value=String(t.projectId));await vu(t.projectId,t.bookId);au(!1);pe("Connected");sf("Bound manuscript detected.");await by();return}}catch{}ii=null;vf();bc();kr()?await vu(kr().projectId,null):c(d,"Select a project first");ht("Select a Project and Book to begin.");au(!0)},ky=async n=>{ii=null,sp(),lf(),vf(),nt("Not detected"),k("Manual"),pe("Connected"),v(s.projectId,null),v(s.bookId,null),p&&(p.value=""),c(ft,"Select a project first"),bc(),ut&&(ut.value=""),c(d,"Select a project first"),ht(n||"Select a Project and Book to begin."),t(n||"Manuscript unlinked."),dt("No resolved PlotDirector scene."),au(!0),lc()},pg=async()=>{console.debug("Word Companion unlink clicked");t("Preparing to unlink manuscript...");const n=rd();if(!n){t("This Word document is not linked.");return}if(!n.documentGuid||!Number.isInteger(n.bookId)||n.bookId<=0){t("Unable to unlink: bound document metadata is incomplete.");return}const r=await cg();if(!r){t("Unlink cancelled.");return}t("Unlinking manuscript...");hr&&(hr.disabled=!0,hr.textContent="Unlinking...");try{await ct("/api/word-companion/manuscript/unlink",{documentGuid:n.documentGuid,bookId:n.bookId});await kv();await ky("Manuscript unlinked.")}catch(i){console.error("Unable to unlink manuscript binding.",i);t("Unable to unlink from PlotDirector.");const r=window.confirm("PlotDirector could not be updated, but you can still remove the binding from this Word document.\n\nYou may need to unlink the Book manually in PlotDirector later.\n\nRemove binding from this Word document only?");if(r)try{await kv();await ky("Word metadata removed. Unlink the Book manually in PlotDirector if it still shows as linked.")}catch(n){console.error("Unable to remove Word document metadata.",n);t(`Unable to remove Word metadata: ${o(n)}`)}else t(`Unable to unlink manuscript: ${o(i)}`)}finally{hr&&(hr.disabled=!1,hr.textContent="Unlink this Word document");lc()}},ctt=(n,t)=>lt(n,{method:"PATCH",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)}),wg=(n,t)=>lt(n,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)}),vs=n=>n?[...n.querySelectorAll("input[type='checkbox']:checked")].map(n=>Number.parseInt(n.value||"",10)).filter(n=>Number.isInteger(n)&&n>0):[],dy=()=>{const n=Number.parseInt(lr?.value||"",10);return Number.isInteger(n)&&n>0?n:null},bg=()=>{const n=new Set(vs(df)),t=new Set(vs(gf));st={characters:st.characters.map(t=>({...t,linked:n.has(Number.parseInt(fs(t,"character"),10))})),assets:st.assets.map(n=>({...n,linked:t.has(Number.parseInt(fs(n,"asset"),10))})),locations:st.locations,primaryLocationId:dy()}},kg=async()=>{if(!u)return!1;if(!bt||!et||!window.Word||typeof window.Word.run!="function")return li(!0,"The Word cursor is now in a different scene. Refresh Current Scene before saving."),!1;try{const n=await window.Word.run(async n=>await wf(n)),r=n.currentChapter?.title||"",f=n.currentScene?.title||"",t=n.currentScene?.anchorId||null,i=n.currentChapter?.anchorId||null,o=Number.isInteger(t)&&t===u,s=Number.isInteger(i)&&i===e,h=it(f,"scene").toLocaleLowerCase()===it(go||tt,"scene").toLocaleLowerCase(),c=it(r,"chapter").toLocaleLowerCase()===it(oc||w,"chapter").toLocaleLowerCase(),l=o||h&&(s||c);return l?(li(!1),!0):(li(!0,"The Word cursor is now in a different scene. Refresh Current Scene before saving."),!1)}catch(n){return console.error("Unable to verify current Word scene before saving.",n),f({lastError:o(n)}),li(!0,"The Word cursor is now in a different scene. Refresh Current Scene before saving."),!1}},dg=async()=>{if(!u){su("Link a PlotDirector scene first.");return}if(ot||!await kg()){su("The Word cursor is now in a different scene. Refresh Current Scene before saving.");return}ki&&(ki.disabled=!0,ki.textContent="Saving...");try{await wg(`/api/word-companion/scenes/${u}/links`,{characterIds:vs(df),assetIds:vs(gf),locationIds:[],primaryLocationId:dy()});bg();su("Scene links saved successfully.");f({lastError:"-"})}catch(n){su("Unable to save scene links.");f({lastError:o(n)})}finally{ki&&(ki.disabled=!u||ot,ki.textContent="Save Scene Links")}},gg=(n,t)=>{const i=ty(n);return i.find(n=>ny(n.tag,t))||null},no=(n,t,i,r)=>{const f=gg(n,r),u=f||n.getRange().insertContentControl();return u.title=t,u.tag=i,u.appearance="Hidden",u},gy=async(n="PlotDirector IDs attached successfully.")=>{if(!u||!e)return t("No resolved PlotDirector scene."),!1;const r=g&&g!==u||b&&b!==e;if(r&&!window.confirm("This scene is already linked. Replace the existing PlotDirector link?"))return!1;if(!bt||!et||!window.Word||typeof window.Word.run!="function")return t("Word document access is unavailable."),!1;ai&&(ai.disabled=!0,ai.textContent="Attaching...");try{return await window.Word.run(async n=>{const t=await wf(n);if(!t.currentChapter||!t.currentScene)throw new Error("No scene detected in Word. Use Heading 2 for scenes.");const i=t.bodyParagraphs[t.currentChapter.paragraphIndex],r=t.bodyParagraphs[t.currentScene.paragraphIndex];if(!i||!r)throw new Error("Unable to locate the current Word headings.");no(i,"PlotDirector Chapter",`PD-CHAPTER-${e}`,"PD-CHAPTER");no(r,"PlotDirector Scene",`PD-SCENE-${u}`,"PD-SCENE");await n.sync()}),b=e,g=u,kt=!1,ir="SceneID Anchor",te="Anchor",t(n),f({lastError:"-"}),ur(),!0}catch(i){return console.error("Unable to attach PlotDirector IDs.",i),t("Unable to attach PlotDirector IDs."),f({lastError:o(i)}),ur(),!1}finally{ai&&(ai.textContent=g===u?"Attach PlotDirector IDs":"Reattach PlotDirector IDs",ur())}},np=async n=>{if(!e)return yi("No resolved PlotDirector chapter."),!1;if(!bt||!et||!window.Word||typeof window.Word.run!="function")return yi("Word document access is unavailable."),!1;try{return await window.Word.run(async n=>{const t=await wf(n);if(!t.currentChapter)throw new Error("No chapter detected in Word. Use Heading 1 for chapters.");const i=t.bodyParagraphs[t.currentChapter.paragraphIndex];if(!i)throw new Error("Unable to locate the current Word chapter heading.");no(i,"PlotDirector Chapter",`PD-CHAPTER-${e}`,"PD-CHAPTER");await n.sync()}),b=e,kt=!1,ir="ChapterID Anchor",te="Chapter anchor",yi(n),t(n),f({lastError:"-"}),ur(),!0}catch(i){return console.error("Unable to attach PlotDirector chapter ID.",i),yi("Unable to attach PlotDirector chapter ID."),f({lastError:o(i)}),ur(),!1}},nn=async()=>{await gy()},tp=async n=>{we(),await ro(),t(n),yi(n)},tl=n=>{yo&&(yo(n),yo=null),gi?.close?gi.close():gi&&(gi.hidden=!0)},tn=(t,i)=>(n(gw,`"${t}"`),n(nb,`"${i}"`),!gi)?Promise.resolve(window.confirm(`Create PlotDirector chapter: + +"${t}" + +in book: + +"${i}" + +?`)):new Promise(n=>{yo=n,gi.showModal?gi.showModal():gi.hidden=!1}),rn=async()=>{const n=h();if(!n||!w||e){rs("Chapter not found in PlotDirector.");return}const t=cu(w),i=await tn(t,br(n)||"selected book");if(i){vt&&(vt.disabled=!0,vt.textContent="Creating...");try{const r=Number.isInteger(ne)&&ne>0?ne*10:null,i=await ct(`/api/word-companion/books/${n.bookId}/chapters`,{title:t,sortOrder:r});if(!i?.chapterId)throw new Error("Chapter creation did not return a chapter ID.");ou();wr(null,i.chapterId,"Manual chapter link");const u=await np("Chapter created and linked successfully.");if(!u)return;await tp("Chapter created and linked successfully.")}catch(r){yi("Unable to create chapter.");f({lastError:o(r)})}finally{vt&&(vt.disabled=!yc(),vt.textContent="Create Chapter in PlotDirector")}}},ip=()=>{if(co){const n=cu(ho?.value||"").toLocaleLowerCase(),t=hh.filter(t=>!n||String(t.title||"").toLocaleLowerCase().includes(n));if(co.innerHTML="",re=null,oi&&(oi.disabled=!0),t.length===0){const n=document.createElement("p");n.className="word-companion-empty-list";n.textContent="No chapters found.";co.append(n);return}for(const n of t){const i=Number.parseInt(n.chapterId||"",10);if(Number.isInteger(i)&&!(i<=0)){const r=document.createElement("label");r.className="word-companion-scene-option";const t=document.createElement("input");t.type="radio";t.name="word-companion-link-chapter";t.value=String(i);t.addEventListener("change",()=>{re=i,oi&&(oi.disabled=!1)});const u=document.createElement("span");u.textContent=n.title||"Untitled chapter";r.append(t,u);co.append(r)}}}},un=async()=>{const t=h();if(!t||!w||e){rs("Chapter not found in PlotDirector.");return}yt&&(yt.disabled=!0,yt.textContent="Loading...");try{hh=await lv(t.bookId);re=null;ho&&(ho.value="");n(fh,"");ip();vr?.showModal?vr.showModal():vr&&(vr.hidden=!1)}catch(i){yi("Unable to load chapters.");f({lastError:o(i)})}finally{yt&&(yt.disabled=!yc(),yt.textContent="Link to Existing Chapter")}},rp=()=>{vr?.close?vr.close():vr&&(vr.hidden=!0)},fn=async()=>{const t=hh.find(n=>n.chapterId===re);if(!t){n(fh,"Select a chapter.");return}oi&&(oi.disabled=!0,oi.textContent="Linking...");try{wr(null,t.chapterId,"Manual chapter link");const n=await np("Chapter linked successfully.");if(!n)return;rp();await tp("Chapter linked successfully.")}catch(i){n(fh,"Unable to link chapter.");yi("Unable to link chapter.");f({lastError:o(i)})}finally{oi&&(oi.disabled=!re,oi.textContent="Link Chapter")}},up=async(n,t,i)=>{const r=h();if(!r)return hu("Select a PlotDirector book first."),!1;await ss(r.bookId,n,t,"Manual link","Title Match");const u=await gy(i);return u?(hu(i),us(),!0):!1},il=n=>{vo&&(vo(n),vo=null),di?.close?di.close():di&&(di.hidden=!0)},en=(t,i)=>(n(pw,`"${t}"`),n(ww,`"${i}"`),!di)?Promise.resolve(window.confirm(`Create PlotDirector scene: + +"${t}" + +within chapter: + +"${i}" + +?`)):new Promise(n=>{vo=n,di.showModal?di.showModal():di.hidden=!1}),on=async()=>{const n=h();if(!n||!tt){be("Scene not found in PlotDirector.");return}const t=it(tt,"scene"),i=it(w,"chapter"),r=await en(t,i);if(r){pt&&(pt.disabled=!0,pt.textContent="Creating...");try{const r=await vv();if(!r){be("This chapter does not exist in PlotDirector. Create or link the chapter first.");return}const i=await ct(`/api/word-companion/books/${n.bookId}/chapters/${r.chapterId}/scenes`,{sceneTitle:t});if(!i?.sceneId||!i?.chapterId)throw new Error("Scene creation did not return a scene ID.");ou();await up(i.sceneId,i.chapterId,"Scene created and linked successfully.")}catch(u){hu("Unable to create scene.");f({lastError:o(u)})}finally{pt&&(pt.disabled=!pc(),pt.textContent="Create Scene in PlotDirector")}}},fp=()=>{if(so){const n=cu(oo?.value||"").toLocaleLowerCase(),t=sh.filter(t=>!n||String(t.title||"").toLocaleLowerCase().includes(n));if(so.innerHTML="",ie=null,ei&&(ei.disabled=!0),t.length===0){const n=document.createElement("p");n.className="word-companion-empty-list";n.textContent="No scenes found.";so.append(n);return}for(const n of t){const i=Number.parseInt(n.sceneId||"",10);if(Number.isInteger(i)&&!(i<=0)){const r=document.createElement("label");r.className="word-companion-scene-option";const t=document.createElement("input");t.type="radio";t.name="word-companion-link-scene";t.value=String(i);t.addEventListener("change",()=>{ie=i,ei&&(ei.disabled=!1)});const u=document.createElement("span");u.textContent=n.title||"Untitled scene";r.append(t,u);so.append(r)}}}},sn=async()=>{const t=h();if(!t||!tt){be("Scene not found in PlotDirector.");return}wt&&(wt.disabled=!0,wt.textContent="Loading...");try{const r=await os(t.bookId),i=av(r);if(!i){be("This chapter does not exist in PlotDirector. Create or link the chapter first.");return}sh=Array.isArray(i.scenes)?i.scenes:[];ie=null;oo&&(oo.value="");n(uh,"");fp();ar?.showModal?ar.showModal():ar&&(ar.hidden=!1)}catch(i){hu("Unable to load scenes.");f({lastError:o(i)})}finally{wt&&(wt.disabled=!pc(),wt.textContent="Link to Existing Scene")}},ep=()=>{ar?.close?ar.close():ar&&(ar.hidden=!0)},hn=async()=>{const i=h(),t=sh.find(n=>n.sceneId===ie);if(!i||!t){n(uh,"Select a scene.");return}ei&&(ei.disabled=!0,ei.textContent="Linking...");try{const n=await vv();if(!n)throw new Error("This chapter does not exist in PlotDirector. Create or link the chapter first.");ep();await up(t.sceneId,n.chapterId,"Scene linked successfully.")}catch(r){n(uh,"Unable to link scene.");hu("Unable to link scene.");f({lastError:o(r)})}finally{ei&&(ei.disabled=!ie,ei.textContent="Link Scene")}},to=()=>{rf=!1,vc(),ve&&(ve=!1,ys(1e3))},op=async(t={})=>{const p=t.source||"manual",w=y();if(!u){dt("No resolved PlotDirector scene.");return}if(!e){dt("No resolved PlotDirector chapter.");return}if(ot){dt("Refresh Current Scene before syncing.");return}const c=h(),l=dr();if(!c||!l){dt("Bound manuscript metadata is unavailable.");return}if(rf){ve=!0;i("Scene sync deferred; sync already running.");return}lf();rf=!0;ve=!1;let r=t.snapshot||null;try{r=ni(r)?r:await ay()}catch(v){dt("Sync failed");f({lastError:o(v)});i("Scene sync failed.");to();return}if(!ni(r)){dt("Waiting for typing to pause");i("Scene sync skipped (stale snapshot).");to();return}const s=r.wordCount;if(!Number.isInteger(s)||s<0){dt("Sync failed");i("Scene sync failed.");to();return}if(vh===r.sceneId&&yh===s){dt("Synced");i("Sync skipped (count unchanged).");to();return}i("Scene word count changed.");cr&&(cr.disabled=!0,cr.textContent="Syncing...");dt("Syncing...");i(`Scene sync started (${p}).`);try{const f=y(),e=await ct("/api/word-companion/runtime/scene-word-count",{documentGuid:r.documentGuid||l,bookId:r.bookId||c.bookId,chapterId:r.chapterId,sceneId:r.sceneId,wordCount:s});if(ci(`Word count sync API: ${Math.round(y()-f)}ms`),!ni(r)){i("Scene sync result ignored (stale snapshot).");return}const t=e?.actualWords??s,u=new Date;vh=r.sceneId;yh=t;n(rh,a(t));n(nh,a(t));n(wa,is(u));n(pa,is(u));dt("Synced");i("Scene sync succeeded.");ci(`Total word count sync: ${Math.round(y()-w)}ms`)}catch(v){dt("Sync failed");f({lastError:o(v)});i("Scene sync failed.")}finally{cr&&(cr.textContent="Sync Current Scene");to()}},ys=(n=4e3)=>{(lf(),u&&e&&!ot&&!rf)&&(ae=window.setTimeout(()=>{ae=null,void op({source:"automatic"})},n))},io=()=>{ye&&(window.clearTimeout(ye),ye=null)},cn=()=>{ph=null,wh="",ef=[],bh=null,kh="",io()},ln=()=>{dh=null,gh="",ru=[],nc=null,tc=""},an=()=>{ic=null,rc="",uu=[],uc=null,fc=""},rl=async(n=false)=>{const t=gt(),r=dr();if(!t||!r)return ef=[],[];if(!n&&ph===t.projectId&&wh===r&&ef.length>0)return ef;const u=await lt(`/api/word-companion/runtime/characters?projectId=${encodeURIComponent(t.projectId)}&documentGuid=${encodeURIComponent(r)}`);return ph=t.projectId,wh=r,ef=Array.isArray(u?.characters)?u.characters:[],i("Character alias cache refreshed."),ef},ul=async(n=false)=>{const t=gt(),r=dr();if(!t||!r)return ru=[],[];if(!n&&dh===t.projectId&&gh===r&&ru.length>0)return ru;const u=await lt(`/api/word-companion/runtime/assets?projectId=${encodeURIComponent(t.projectId)}&documentGuid=${encodeURIComponent(r)}`);return dh=t.projectId,gh=r,ru=Array.isArray(u?.assets)?u.assets:[],i(`Asset cache loaded: ${ru.length} aliases.`),ru},fl=async(n=false)=>{const t=gt(),r=dr();if(!t||!r)return uu=[],[];if(!n&&ic===t.projectId&&rc===r&&uu.length>0)return uu;const u=await lt(`/api/word-companion/runtime/locations?projectId=${encodeURIComponent(t.projectId)}&documentGuid=${encodeURIComponent(r)}`);return ic=t.projectId,rc=r,uu=Array.isArray(u?.locations)?u.locations:[],i(`Location cache loaded: ${uu.length} aliases.`),uu},vn=n=>String(n||"").replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),el=(n,t)=>{const i=String(t||"").trim();if(!i)return!1;const r=vn(i).replace(/\s+/g,"\\s+");return new RegExp(`(^|[^\\p{L}\\p{N}_])${r}(?=$|[^\\p{L}\\p{N}_])`,"iu").test(n)},yn=(n,t)=>{if(!n||!Array.isArray(t)||t.length===0)return[];const i=new Map;for(const r of t){const t=Number.parseInt(r.characterId||"",10);!Number.isInteger(t)||t<=0||i.has(t)||el(n,r.matchText)&&i.set(t,r.name||r.matchText||"Character")}return[...i.entries()].map(([n,t])=>({characterId:n,name:t}))},pn=(n,t)=>{if(!n||!Array.isArray(t)||t.length===0)return[];const i=new Map;for(const r of t){const t=Number.parseInt(r.assetId||"",10);!Number.isInteger(t)||t<=0||i.has(t)||el(n,r.matchText)&&i.set(t,r.name||r.matchText||"Asset")}return[...i.entries()].map(([n,t])=>({assetId:n,name:t}))},wn=n=>String(n||"").trim().split(/\s+/).filter(Boolean).length,bn=n=>{if(n?.excludeFromCompanionDetection)return!1;const t=Number.parseInt(n?.detectionPriority||"0",10);return wn(n?.matchText)>=2||Number.isInteger(t)&&t>=75},kn=(n,t)=>{if(!n||!Array.isArray(t)||t.length===0)return[];const i=new Map;for(const r of t){const t=Number.parseInt(r.locationId||"",10);!Number.isInteger(t)||t<=0||i.has(t)||!bn(r)||el(n,r.matchText)&&i.set(t,r.name||r.matchText||"Location")}return[...i.entries()].map(([n,t])=>({locationId:n,name:t}))},dn=()=>{uf=!1,ff&&(ff=!1,ps(1500))},gn=async()=>{if(ye=null,u&&!ot&&dr()){if(uf){ff=!0;i("Character suggestion detection deferred; detection already running.");return}uf=!0;ff=!1;const r=y();try{const n=await ay();if(!ni(n)){i("Runtime suggestions skipped (stale snapshot).");return}const[w,b,k]=await Promise.all([rl(),ul(),fl()]);if(!ni(n)){i("Runtime suggestions skipped after cache load (stale snapshot).");return}const s=n.sceneText||"",d=y(),o=yn(s,w);ci(`Character detection: ${Math.round(y()-d)}ms`);const a=o.map(n=>n.characterId),h=a.slice().sort((n,t)=>n-t).join(","),g=y(),f=pn(s,b);ci(`Asset detection: ${Math.round(y()-g)}ms`);const v=f.map(n=>n.assetId),c=v.slice().sort((n,t)=>n-t).join(","),nt=y(),e=kn(s,k);ci(`Location detection: ${Math.round(y()-nt)}ms`);const p=e.map(n=>n.locationId),l=p.slice().sort((n,t)=>n-t).join(",");if(h&&(bh!==u||kh!==h)){if(!ni(n)){i("Character suggestions skipped before submit (stale snapshot).");return}const u=y(),r=await ct("/api/word-companion/runtime/character-suggestions",{documentGuid:n.documentGuid,sceneId:n.sceneId,characterIds:a});if(ci(`Character suggestion API: ${Math.round(y()-u)}ms`),!ni(n)){i("Character suggestion result ignored (stale snapshot).");return}if(bh=n.sceneId,kh=h,(r?.createdCount||0)>0){const n=o.slice(0,3).map(n=>n.name).join(", "),i=o.length>3?` +${o.length-3}`:"";t(n?`Characters detected: ${n}${i}`:r.message)}i(r?.message||"Character suggestions submitted.")}else i("Character suggestions skipped.");if(c&&(nc!==u||tc!==c)){if(!ni(n)){i("Asset suggestions skipped before submit (stale snapshot).");return}i(`Asset matches found: ${f.map(n=>n.name).join(", ")}`);const u=y(),r=await ct("/api/word-companion/runtime/asset-suggestions",{documentGuid:n.documentGuid,sceneId:n.sceneId,assetIds:v});if(ci(`Asset suggestion API: ${Math.round(y()-u)}ms`),!ni(n)){i("Asset suggestion result ignored (stale snapshot).");return}if(nc=n.sceneId,tc=c,(r?.createdCount||0)>0){const n=f.slice(0,3).map(n=>n.name).join(", "),i=f.length>3?` +${f.length-3}`:"";t(n?`Assets detected: ${n}${i}`:r.message)}i(r?.message||"Asset suggestions submitted.")}else i("Asset suggestions skipped.");if(l&&(uc!==u||fc!==l)){if(!ni(n)){i("Location suggestions skipped before submit (stale snapshot).");return}i(`Location matches found: ${e.map(n=>n.name).join(", ")}`);const u=y(),r=await ct("/api/word-companion/runtime/location-suggestions",{documentGuid:n.documentGuid,sceneId:n.sceneId,locationIds:p});if(ci(`Location suggestion API: ${Math.round(y()-u)}ms`),!ni(n)){i("Location suggestion result ignored (stale snapshot).");return}if(uc=n.sceneId,fc=l,(r?.createdCount||0)>0){const n=e.slice(0,3).map(n=>n.name).join(", "),i=e.length>3?` +${e.length-3}`:"";t(n?`Locations detected: ${n}${i}`:r.message)}i(r?.message||"Location suggestions submitted.")}else i("Location suggestions skipped.");ci(`Total suggestion pass: ${Math.round(y()-r)}ms`)}catch(n){console.error("Unable to detect runtime suggestions.",n);f({lastError:o(n)})}finally{dn()}}},ps=(n=2500)=>{if(io(),!u||ot||uf){uf&&(ff=!0);return}ye=window.setTimeout(()=>{void gn()},n)},ol=async()=>{const i=gt(),n=h(),r=it(w,"chapter");if(at({projectId:i?.projectId,projectTitle:i?.title,bookId:n?.bookId,bookTitle:n?br(n):undefined,detectedChapter:w,detectedScene:tt,requestChapter:r,requestScene:"",result:"Not run",chapterId:null,sceneId:null}),!n)return nt("Not detected"),t("Select a PlotDirector book first."),!1;if(!w)return nt("Not detected"),t("No chapter detected in Word. Use Heading 1 for chapters."),!1;nt("Not detected");try{const f=await lv(n.bookId),i=b?f.find(n=>n.chapterId===b):null,e=r.toLocaleLowerCase(),o=e?f.find(n=>it(n.title,"chapter").toLocaleLowerCase()===e):null,u=i||o;return u?(we(),wr(null,u.chapterId,i?"Chapter anchor":"Chapter title"),ir=i?"ChapterID Anchor":"Title Match",hf("Chapter linked to PlotDirector"),t("No scene detected in Word. Use Heading 2 for scenes."),at({result:"Chapter matched",chapterId:u.chapterId,sceneId:null}),!0):(wr(null),hf("Chapter not found in PlotDirector."),t("Chapter not found in PlotDirector."),at({result:"Chapter not matched",chapterId:null,sceneId:null}),rs("Chapter not found in PlotDirector."),!1)}catch(u){return nt("Not found in PlotDirector"),f({lastError:o(u)}),t("Unable to load PlotDirector chapter data."),!1}},ro=async()=>{const r=gt(),n=h(),u=it(w,"chapter"),i=it(tt,"scene");if(at({projectId:r?.projectId,projectTitle:r?.title,bookId:n?.bookId,bookTitle:n?br(n):undefined,detectedChapter:w,detectedScene:tt,requestChapter:u,requestScene:i,result:"Not run",chapterId:null,sceneId:null}),cc({anchorType:g?"SceneID Anchor":b?"ChapterID Anchor":"Title Match",storedSceneId:g,storedChapterId:b,resolutionMethod:"-"}),!n)return nt("Not detected"),t("Select a PlotDirector book first."),at({result:"Error"}),!1;if(!w||!tt)return nt("Not detected"),t("No scene detected in Word. Use Heading 2 for scenes."),at({result:"Error"}),!1;nu&&(nu.disabled=!0,nu.textContent="Refreshing...");const s=()=>{nu&&(nu.disabled=!1,nu.textContent="Refresh PlotDirector Scene")};nt("Not detected");t("Resolving PlotDirector scene...");try{if(g){const i=await yk(n.bookId,(n,t)=>t.sceneId===g);if(i)return at({result:"Matched",chapterId:i.chapter.chapterId,sceneId:i.scene.sceneId}),await ss(n.bookId,i.scene.sceneId,i.chapter.chapterId,"Anchor","SceneID Anchor"),!0;kt=!0;ir="SceneID Anchor";hf("Not found in PlotDirector");t("Stored PlotDirector ID is invalid.");at({result:"Error",chapterId:b,sceneId:g});cc({anchorType:"SceneID Anchor",storedSceneId:g,storedChapterId:b,resolutionMethod:"Anchor"})}if(b){const u=await os(n.bookId),t=u.find(n=>n.chapterId===b),r=t?(Array.isArray(t.scenes)?t.scenes:[]).find(n=>it(n.title,"scene").toLocaleLowerCase()===i.toLocaleLowerCase()):null;if(r)return at({result:"Matched",chapterId:t.chapterId,sceneId:r.sceneId}),await ss(n.bookId,r.sceneId,t.chapterId,"Anchor","ChapterID Anchor"),!0;t&&!g?(ir="ChapterID Anchor",wr(null,b,"Chapter anchor"),at({result:"Chapter matched",chapterId:b,sceneId:null})):g||(ir="ChapterID Anchor",at({result:"Chapter anchor not found",chapterId:b,sceneId:null}))}const r=await ct(`/api/word-companion/books/${n.bookId}/resolve-scene`,{chapterTitle:u,sceneTitle:i});if(!r?.matched||!r.sceneId){const u=kt,f=Number.parseInt(r?.chapterId||"",10),o=Number.isInteger(e)&&e>0?e:null,n=Number.isInteger(f)&&f>0?f:o,i=!!r?.chapterMatched||!!n;return nt("Not found in PlotDirector"),kt=u,i&&(wr(null,n,n===o?"Chapter anchor":"Chapter title"),hf("Scene not found in PlotDirector.")),t(u?"Stored PlotDirector ID is invalid.":i?"Scene not found in PlotDirector.":"This chapter does not exist in PlotDirector. Create or link the chapter first."),at({result:"Not matched",chapterId:i?n:r?.chapterId,sceneId:r?.sceneId}),u||(i?(we(),be("Scene not found in PlotDirector.")):(us(),rs("Chapter not found in PlotDirector."))),s(),!1}const f=kt;return at({result:f?"Matched by title fallback":"Matched",chapterId:r.chapterId,sceneId:r.sceneId}),await ss(n.bookId,r.sceneId,r.chapterId,f?"Title fallback":"Title","Title Match"),f&&(kt=!0,t("Stored PlotDirector ID is invalid."),ur()),!0}catch(c){const n=kt;return f({lastError:o(c)}),nt("Not found in PlotDirector"),kt=n,t(n?"Stored PlotDirector ID is invalid.":"Unable to load PlotDirector scene data."),at({result:"Error"}),!1}finally{s()}},ltt=n=>{const t=n.currentChapter?.title||"",i=n.currentScene?.title||"",r=n.currentChapter?.anchorId||null,u=n.currentScene?.anchorId||null;return it(t,"chapter")!==it(w,"chapter")||it(i,"scene")!==it(tt,"scene")||r!==b||u!==g},ntt=async()=>{if(iu=null,ko){tf=!0;i("Runtime update deferred; update already running.");return}if(!gu||!h()){pr=!1;i("Runtime update skipped.");return}if(!bt||!et||!window.Word||typeof window.Word.run!="function"){pr=!1;k("Manual");t("Automatic scene tracking is unavailable in this Word version. Use Refresh Current Scene.");return}if(!l){pr=!1;k("Manual");t("Document structure is not cached yet. Use Refresh Current Scene.");return}const n=Date.now()-rv;if(pr&&n{iu&&window.clearTimeout(iu),iu=window.setTimeout(ntt,Math.max(0,n)),k("Waiting for typing to pause"),i("Runtime update scheduled.")},ttt=(n="selection",t=le)=>{if(pr=!0,rv=Date.now(),he+=1,ce=null,lf(),io(),rf&&(ve=!0),uf&&(ff=!0),i(`${n} changed.`),ko){tf=!0;k("Waiting for typing to pause");return}sl(t)},sp=()=>{iu&&(window.clearTimeout(iu),iu=null),pr=!1,tf=!1},hp=()=>{ttt("Selection")},itt=()=>{if(ah&&window.Office?.context?.document&&typeof window.Office.context.document.removeHandlerAsync=="function"&&window.Office?.EventType?.DocumentSelectionChanged)try{window.Office.context.document.removeHandlerAsync(window.Office.EventType.DocumentSelectionChanged,{handler:hp});ah=!1}catch(n){console.warn("Unable to remove Word selection tracking handler.",n)}},rtt=()=>{if(gu){if(!window.Office?.context?.document||typeof window.Office.context.document.addHandlerAsync!="function"||!window.Office?.EventType?.DocumentSelectionChanged){k("Manual");t("Automatic scene tracking is unavailable in this Word version. Use Refresh Current Scene.");return}try{window.Office.context.document.addHandlerAsync(window.Office.EventType.DocumentSelectionChanged,hp,n=>{n?.status===window.Office.AsyncResultStatus.Succeeded?(ah=!0,k("Live")):(k("Manual"),t("Automatic scene tracking is unavailable in this Word version. Use Refresh Current Scene."))});window.addEventListener("beforeunload",itt)}catch(n){console.warn("Unable to register Word selection tracking handler.",n);k("Manual");t("Automatic scene tracking is unavailable in this Word version. Use Refresh Current Scene.")}}},utt=async()=>{if(!h()){t("Select a PlotDirector book first.");nt("Not detected");return}sp();sv(!0);k("Updating...");try{await Promise.all([rl(!0),ul(!0),fl(!0)]);const n=await vy();if(!n)return;if(li(!1),!tt){await ol();k("Current scene updated");return}await ro();k("Current scene updated")}finally{sv(!1)}},vu=async(n,t)=>{if(l=null,ou(),!n){ti=[];c(ft,"Select a project first");c(d,"Select a project first");sf("");v(s.bookId,null);pi();nt("Not detected");af();return}c(ft,"Loading books...");sf("");nt("Not detected");af("Select a book to analyse manuscript structure.");try{const r=await lt(`/api/word-companion/projects/${n}/books`);if(ti=Array.isArray(r.books)?r.books:[],ti.length===0){c(ft,"No books available.");c(d,"No books available.");sf("No books available.");v(s.bookId,null);pi();ht("No books available.");return}cs(ft,"Choose a book",ti.map(n=>({...n,displayTitle:br(n)})),"bookId","displayTitle");const i=ti.find(n=>n.bookId===t);i&&ft?(ft.value=String(i.bookId),v(s.bookId,i.bookId)):v(s.bookId,null);nd(i?.bookId||t);pi();nt(h()?"Not detected":"Not detected");af(h()?"Ready to analyse manuscript structure.":"Select a book to analyse manuscript structure.");ht(lu()?"Ready to scan manuscript structure.":"Select a Book to begin.")}catch{ti=[];c(ft,"Unable to load books.");c(d,"Unable to load books.");sf("Unable to load books.");v(s.bookId,null);pi();nt("Not detected");af("Unable to load books.");ht("Unable to load books.")}},ftt=async()=>{c(p,"Loading projects...");c(ft,"Select a project first");c(ut,"Loading projects...");c(d,"Select a project first");hc("");sf("");try{const t=await lt("/api/word-companion/projects");if(tr=Array.isArray(t.projects)?t.projects:[],tr.length===0){c(p,"No projects available.");c(ut,"No projects available.");hc("No projects available.");v(s.projectId,null);v(s.bookId,null);pi();ht("No projects available.");return}cs(p,"Choose a project",tr,"projectId","title");bc();const i=hs(s.projectId),n=tr.find(n=>n.projectId===i);n&&p?(p.value=String(n.projectId),ut&&(ut.value=String(n.projectId)),v(s.projectId,n.projectId),await vu(n.projectId,hs(s.bookId))):(v(s.projectId,null),v(s.bookId,null),pi(),ht("Select a Project and Book to begin."))}catch{tr=[];ti=[];c(p,"Unable to connect to PlotDirector.");c(ft,"Select a project first");c(ut,"Unable to connect to PlotDirector.");c(d,"Select a project first");pe("Unable to connect to PlotDirector.");hc("Unable to connect to PlotDirector.");v(s.projectId,null);v(s.bookId,null);pi();ht("Unable to connect to PlotDirector.")}};for(const n of bs)n.addEventListener("click",()=>ev(n.dataset.tabButton));ov();p?.addEventListener("change",async()=>{const n=Number.parseInt(p.value||"",10);vf();ut&&Number.isInteger(n)&&n>0&&(ut.value=String(n));v(s.projectId,Number.isInteger(n)&&n>0?n:null);v(s.bookId,null);nt("Not detected");af();ht("Select a Book to begin.");await vu(n,null);yf()});ft?.addEventListener("change",()=>{const n=Number.parseInt(ft.value||"",10);vf();v(s.bookId,Number.isInteger(n)&&n>0?n:null);d&&Number.isInteger(n)&&n>0&&(d.value=String(n));pi();nt("Not detected");af(h()?"Ready to analyse manuscript structure.":"Select a book to analyse manuscript structure.");ht(lu()?"Ready to scan manuscript structure.":"Select a Book to begin.");yf()});ut?.addEventListener("change",async()=>{const n=Number.parseInt(ut.value||"",10);vf();p&&Number.isInteger(n)&&n>0&&(p.value=String(n));v(s.projectId,Number.isInteger(n)&&n>0?n:null);v(s.bookId,null);ht("Select a Book to begin.");await vu(n,null);yf()});d?.addEventListener("change",()=>{const n=Number.parseInt(d.value||"",10);vf();ft&&Number.isInteger(n)&&n>0&&(ft.value=String(n));v(s.bookId,Number.isInteger(n)&&n>0?n:null);pi();ht(lu()?"Ready to scan manuscript structure.":"Select a Book to begin.");yf()});bf?.addEventListener("input",rt);uo?.addEventListener("click",async()=>{const n=kr(),t=String(bf?.value||"").trim();if(!n||!t){rt();return}uo.disabled=!0;ri("Creating Book...");try{const i=await ct(`/api/word-companion/projects/${n.projectId}/books`,{title:t});bf&&(bf.value="");p&&(p.value=String(n.projectId));await vu(n.projectId,i.bookId);d&&(d.value=String(i.bookId));ri("Ready to scan manuscript structure.")}catch(i){console.error("Unable to create Book from Word Companion.",i);ri(`Unable to create Book: ${o(i)}`)}finally{rt()}});er?.addEventListener("click",sg);ds?.addEventListener("click",lg);fo?.addEventListener("click",ag);ll?.addEventListener("click",()=>as("Character creation skipped."));al?.addEventListener("click",()=>as());pp?.addEventListener("click",()=>au(!1));bp?.addEventListener("click",()=>ht("Import cancelled."));gr?.addEventListener("click",vy);sr?.addEventListener("click",yy);gs?.addEventListener("click",eg);rb?.addEventListener("click",()=>nl(!0));ub?.addEventListener("click",()=>nl(!1));nr?.addEventListener("cancel",n=>{n.preventDefault(),nl(!1)});nu?.addEventListener("click",ro);eo?.addEventListener("click",utt);cr?.addEventListener("click",()=>op({source:"manual"}));ki?.addEventListener("click",dg);ai?.addEventListener("click",nn);hr?.addEventListener("click",pg);vt?.addEventListener("click",rn);yt?.addEventListener("click",un);ho?.addEventListener("input",ip);oi?.addEventListener("click",fn);dw?.addEventListener("click",rp);tb?.addEventListener("click",()=>tl(!0));ib?.addEventListener("click",()=>tl(!1));gi?.addEventListener("cancel",n=>{n.preventDefault(),tl(!1)});pt?.addEventListener("click",on);wt?.addEventListener("click",sn);oo?.addEventListener("input",fp);ei?.addEventListener("click",hn);yw?.addEventListener("click",ep);bw?.addEventListener("click",()=>il(!0));kw?.addEventListener("click",()=>il(!1));di?.addEventListener("cancel",n=>{n.preventDefault(),il(!1)});tu?.addEventListener("click",og);const ett=gu?ftt():Promise.resolve();if(gu||(c(p,"Please sign in to PlotDirector."),c(ft,"Please sign in to PlotDirector."),c(ut,"Please sign in to PlotDirector."),c(d,"Please sign in to PlotDirector."),pi()),!window.Office||typeof window.Office.onReady!="function"){sc("Word Companion ready");t("Word document access is unavailable.");k("Manual");f({host:"Unavailable",platform:"Unavailable",ready:"No",wordApi:"No"});wv().catch(n=>{console.debug("Word Companion presence unavailable.",n)});return}yv().then(async()=>{sc("Word Companion ready"),await ett,await yg(),await wv(),et?rtt():(t("Word document access is unavailable."),k("Manual"))}).catch(n=>{console.error("Office readiness check failed.",n),pe("Unable to connect to PlotDirector."),sc("Word Companion unavailable"),t("Word document access is unavailable."),f({ready:"No",wordApi:"No",lastScan:"Failure"})})})(); \ No newline at end of file diff --git a/PlotLine/wwwroot/js/word-companion-presence.js b/PlotLine/wwwroot/js/word-companion-presence.js new file mode 100644 index 0000000..79848f6 --- /dev/null +++ b/PlotLine/wwwroot/js/word-companion-presence.js @@ -0,0 +1,80 @@ +(() => { + 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 })); +})();