From a70527f3a4aa071432adf5b772caa7a3d88256d8 Mon Sep 17 00:00:00 2001 From: Nick Beckley Date: Fri, 3 Jul 2026 22:20:14 +0100 Subject: [PATCH] Phase 20D Implement website-led manuscript scanning via the Word Companion. --- PlotLine/Controllers/OnboardingController.cs | 7 + PlotLine/Hubs/WordCompanionFollowHub.cs | 92 ++++++- PlotLine/Models/ManuscriptScanModels.cs | 105 ++++++++ PlotLine/Program.cs | 1 + .../Services/ManuscriptScanPreviewStore.cs | 195 ++++++++++++++ PlotLine/Services/OnboardingService.cs | 76 ++++++ .../Services/WordCompanionPresenceMonitor.cs | 10 + .../Services/WordCompanionPresenceService.cs | 14 + PlotLine/ViewModels/OnboardingViewModels.cs | 40 +++ PlotLine/Views/Onboarding/Index.cshtml | 100 ++++++- PlotLine/Views/Onboarding/ScanReview.cshtml | 102 ++++++++ PlotLine/wwwroot/css/onboarding.css | 155 +++++++++++ PlotLine/wwwroot/js/word-companion-host.js | 244 ++++++++++++++++++ .../wwwroot/js/word-companion-host.min.js | 8 +- .../wwwroot/js/word-companion-presence.js | 101 +++++++- 15 files changed, 1239 insertions(+), 11 deletions(-) create mode 100644 PlotLine/Models/ManuscriptScanModels.cs create mode 100644 PlotLine/Services/ManuscriptScanPreviewStore.cs create mode 100644 PlotLine/Views/Onboarding/ScanReview.cshtml diff --git a/PlotLine/Controllers/OnboardingController.cs b/PlotLine/Controllers/OnboardingController.cs index 59b33a3..8c15fe8 100644 --- a/PlotLine/Controllers/OnboardingController.cs +++ b/PlotLine/Controllers/OnboardingController.cs @@ -17,6 +17,13 @@ public sealed class OnboardingController(IOnboardingService onboarding) : Contro return View(model); } + [HttpGet("scan-review")] + public async Task ScanReview(Guid? previewId) + { + var model = await onboarding.GetScanReviewAsync(previewId); + return model is null ? NotFound() : View(model); + } + [HttpPost("welcome")] [ValidateAntiForgeryToken] public async Task Welcome() diff --git a/PlotLine/Hubs/WordCompanionFollowHub.cs b/PlotLine/Hubs/WordCompanionFollowHub.cs index 315db79..6ebefb2 100644 --- a/PlotLine/Hubs/WordCompanionFollowHub.cs +++ b/PlotLine/Hubs/WordCompanionFollowHub.cs @@ -1,5 +1,6 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.SignalR; +using PlotLine.Data; using PlotLine.Models; using PlotLine.Services; using System.Security.Claims; @@ -7,7 +8,10 @@ using System.Security.Claims; namespace PlotLine.Hubs; [Authorize] -public sealed class WordCompanionFollowHub(IWordCompanionPresenceService presence) : Hub +public sealed class WordCompanionFollowHub( + IWordCompanionPresenceService presence, + IOnboardingRepository onboarding, + IManuscriptScanPreviewStore scanStore) : Hub { public override async Task OnConnectedAsync() { @@ -47,12 +51,94 @@ public sealed class WordCompanionFollowHub(IWordCompanionPresenceService presenc return status; } + public async Task StartOnboardingManuscriptScan() + { + var userId = RequireUserId(); + var state = await onboarding.GetAsync(userId); + if (state is null + || !IsMicrosoftWordState(state) + || !state.ProjectID.HasValue + || !state.BookID.HasValue) + { + throw new HubException("Finish choosing a Project and Book before scanning."); + } + + var presenceStatus = await presence.GetStatusAsync(userId); + if (!presenceStatus.IsConnected) + { + throw new HubException("The Word Companion is not connected. Open Word and try again."); + } + + if (!presenceStatus.DocumentOpen) + { + throw new HubException("Open your manuscript in Word before scanning."); + } + + var companionConnectionId = await presence.GetCompanionConnectionIdAsync(userId); + if (string.IsNullOrWhiteSpace(companionConnectionId)) + { + throw new HubException("The Word Companion connection is not ready. Try reconnecting the Companion."); + } + + var scanState = await scanStore.StartAsync(userId, state.UserOnboardingStateID, state.ProjectID.Value, state.BookID.Value); + await Clients.Group(PresenceGroup(userId)).SendAsync("OnboardingScanStateChanged", scanState); + await Clients.Client(companionConnectionId).SendAsync("ScanCurrentDocument", new ManuscriptScanCommand + { + UserID = userId, + OnboardingID = state.UserOnboardingStateID, + ProjectID = state.ProjectID.Value, + BookID = state.BookID.Value, + RequestedUtc = DateTime.UtcNow + }); + return scanState; + } + + public async Task ReportOnboardingScanProgress(ManuscriptScanProgress progress) + { + var userId = RequireUserId(); + var scanState = await scanStore.ProgressAsync(userId, progress); + await Clients.Group(PresenceGroup(userId)).SendAsync("OnboardingScanProgress", scanState); + return scanState; + } + + public async Task CompleteOnboardingScan(ManuscriptScanPreview preview) + { + var userId = RequireUserId(); + var state = await onboarding.GetAsync(userId); + if (state is null + || state.UserOnboardingStateID != preview.OnboardingID + || state.ProjectID != preview.ProjectID + || state.BookID != preview.BookID) + { + throw new HubException("The scan result does not match your onboarding setup."); + } + + var scanState = await scanStore.CompleteAsync(userId, preview); + await Clients.Group(PresenceGroup(userId)).SendAsync("OnboardingScanCompleted", scanState); + return scanState; + } + + public async Task FailOnboardingScan(ManuscriptScanFailure failure) + { + var userId = RequireUserId(); + var scanState = await scanStore.FailAsync(userId, failure.OnboardingID, failure.Message); + await Clients.Group(PresenceGroup(userId)).SendAsync("OnboardingScanFailed", scanState); + return scanState; + } + 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); + var failedScans = await scanStore.FailRunningForUserAsync( + status.UserID, + "The Word Companion disconnected before the scan finished. Reopen Word and try again."); + foreach (var failedScan in failedScans) + { + await Clients.Group(PresenceGroup(status.UserID)).SendAsync("OnboardingScanFailed", failedScan); + } } await base.OnDisconnectedAsync(exception); @@ -65,4 +151,8 @@ public sealed class WordCompanionFollowHub(IWordCompanionPresenceService presenc => int.TryParse(Context.User?.FindFirstValue(ClaimTypes.NameIdentifier), out userId); private static string PresenceGroup(int userId) => $"word-companion-presence:{userId}"; + + private static bool IsMicrosoftWordState(UserOnboardingState state) + => string.Equals(state.WritingSoftware, WritingSoftwareValues.MicrosoftWord, StringComparison.Ordinal) + || state.UsesWordCompanion == true; } diff --git a/PlotLine/Models/ManuscriptScanModels.cs b/PlotLine/Models/ManuscriptScanModels.cs new file mode 100644 index 0000000..a4f03df --- /dev/null +++ b/PlotLine/Models/ManuscriptScanModels.cs @@ -0,0 +1,105 @@ +namespace PlotLine.Models; + +public static class ManuscriptScanSources +{ + public const string WordCompanion = "WordCompanion"; +} + +public static class ManuscriptScanStatuses +{ + public const string NotStarted = "NotStarted"; + public const string Running = "Running"; + public const string Complete = "Complete"; + public const string Failed = "Failed"; +} + +public sealed class ManuscriptScanCommand +{ + public int UserID { get; init; } + public int OnboardingID { get; init; } + public int ProjectID { get; init; } + public int BookID { get; init; } + public DateTime RequestedUtc { get; init; } +} + +public sealed class ManuscriptScanProgress +{ + public int? UserID { get; init; } + public int OnboardingID { get; init; } + public string Message { get; init; } = string.Empty; + public int? PercentComplete { get; init; } + public int ChapterCount { get; init; } + public int SceneCount { get; init; } + public int CharacterCandidateCount { get; init; } + public int TotalWordCount { get; init; } +} + +public sealed class ManuscriptScanFailure +{ + public int? UserID { get; init; } + public int OnboardingID { get; init; } + public string Message { get; init; } = string.Empty; +} + +public sealed class ManuscriptScanPreview +{ + public Guid PreviewID { get; init; } = Guid.NewGuid(); + public int UserID { get; init; } + public int OnboardingID { get; init; } + public int ProjectID { get; init; } + public int BookID { get; init; } + public string Source { get; init; } = ManuscriptScanSources.WordCompanion; + public string? DocumentTitle { get; init; } + public string? CompanionDocumentIdentifier { get; init; } + public int TotalWordCount { get; init; } + public int ChapterCount { get; init; } + public int SceneCount { get; init; } + public int CharacterCandidateCount { get; init; } + public DateTime CreatedUtc { get; init; } = DateTime.UtcNow; + public IReadOnlyList Chapters { get; init; } = []; + public IReadOnlyList Scenes { get; init; } = []; + public IReadOnlyList CharacterCandidates { get; init; } = []; +} + +public sealed class ManuscriptScanChapterPreview +{ + public string TemporaryChapterKey { get; init; } = string.Empty; + public int ChapterNumber { get; init; } + public string Title { get; init; } = string.Empty; + public int WordCount { get; init; } + public int? StartPosition { get; init; } + public int? ExistingChapterID { get; init; } +} + +public sealed class ManuscriptScanScenePreview +{ + public string TemporarySceneKey { get; init; } = string.Empty; + public string TemporaryChapterKey { get; init; } = string.Empty; + public int SceneNumberWithinChapter { get; init; } + public string? Title { get; init; } + public int WordCount { get; init; } + public string? OpeningTextPreview { get; init; } + public int? StartPosition { get; init; } + public int? ExistingSceneID { get; init; } +} + +public sealed class ManuscriptScanCharacterCandidatePreview +{ + public string TemporaryCharacterKey { get; init; } = string.Empty; + public string Name { get; init; } = string.Empty; + public int MentionCount { get; init; } + public string? SuggestedImportance { get; init; } +} + +public sealed class ManuscriptScanState +{ + public string Status { get; init; } = ManuscriptScanStatuses.NotStarted; + public string Message { get; init; } = string.Empty; + public int? PercentComplete { get; init; } + public int ChapterCount { get; init; } + public int SceneCount { get; init; } + public int CharacterCandidateCount { get; init; } + public int TotalWordCount { get; init; } + public Guid? PreviewID { get; init; } + public DateTime? UpdatedUtc { get; init; } +} diff --git a/PlotLine/Program.cs b/PlotLine/Program.cs index 91f2d6d..e8c4801 100644 --- a/PlotLine/Program.cs +++ b/PlotLine/Program.cs @@ -171,6 +171,7 @@ public class Program builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddSingleton(); + builder.Services.AddSingleton(); builder.Services.AddHostedService(); builder.Services.AddScoped(); builder.Services.AddScoped(); diff --git a/PlotLine/Services/ManuscriptScanPreviewStore.cs b/PlotLine/Services/ManuscriptScanPreviewStore.cs new file mode 100644 index 0000000..d8ac900 --- /dev/null +++ b/PlotLine/Services/ManuscriptScanPreviewStore.cs @@ -0,0 +1,195 @@ +using System.Collections.Concurrent; +using PlotLine.Models; + +namespace PlotLine.Services; + +public interface IManuscriptScanPreviewStore +{ + Task StartAsync(int userId, int onboardingId, int projectId, int bookId); + Task ProgressAsync(int userId, ManuscriptScanProgress progress); + Task CompleteAsync(int userId, ManuscriptScanPreview preview); + Task FailAsync(int userId, int onboardingId, string message); + Task> FailRunningForUserAsync(int userId, string message); + Task GetStateAsync(int userId, int onboardingId); + Task GetLatestPreviewAsync(int userId, int onboardingId); + Task GetPreviewAsync(int userId, Guid previewId); +} + +public sealed class ManuscriptScanPreviewStore : IManuscriptScanPreviewStore +{ + private readonly ConcurrentDictionary<(int UserId, int OnboardingId), ScanSession> sessions = new(); + private readonly ConcurrentDictionary previews = new(); + + public Task StartAsync(int userId, int onboardingId, int projectId, int bookId) + { + var session = new ScanSession + { + UserID = userId, + OnboardingID = onboardingId, + ProjectID = projectId, + BookID = bookId, + Status = ManuscriptScanStatuses.Running, + Message = "Preparing document scan", + PercentComplete = 0, + UpdatedUtc = DateTime.UtcNow + }; + sessions[(userId, onboardingId)] = session; + return Task.FromResult(ToState(session)); + } + + public Task ProgressAsync(int userId, ManuscriptScanProgress progress) + { + var key = (userId, progress.OnboardingID); + var session = sessions.GetOrAdd(key, _ => new ScanSession + { + UserID = userId, + OnboardingID = progress.OnboardingID, + Status = ManuscriptScanStatuses.Running + }); + + session.Status = ManuscriptScanStatuses.Running; + session.Message = Clean(progress.Message, "Scanning manuscript"); + session.PercentComplete = progress.PercentComplete; + session.ChapterCount = progress.ChapterCount; + session.SceneCount = progress.SceneCount; + session.CharacterCandidateCount = progress.CharacterCandidateCount; + session.TotalWordCount = progress.TotalWordCount; + session.UpdatedUtc = DateTime.UtcNow; + return Task.FromResult(ToState(session)); + } + + public Task CompleteAsync(int userId, ManuscriptScanPreview preview) + { + var stored = preview.WithUser(userId); + previews[stored.PreviewID] = stored; + var session = sessions.GetOrAdd((userId, stored.OnboardingID), _ => new ScanSession + { + UserID = userId, + OnboardingID = stored.OnboardingID, + ProjectID = stored.ProjectID, + BookID = stored.BookID + }); + + session.Status = ManuscriptScanStatuses.Complete; + session.Message = "Scan complete"; + session.PercentComplete = 100; + session.ChapterCount = stored.ChapterCount; + session.SceneCount = stored.SceneCount; + session.CharacterCandidateCount = stored.CharacterCandidateCount; + session.TotalWordCount = stored.TotalWordCount; + session.PreviewID = stored.PreviewID; + session.UpdatedUtc = stored.CreatedUtc; + return Task.FromResult(ToState(session)); + } + + public Task FailAsync(int userId, int onboardingId, string message) + { + var session = sessions.GetOrAdd((userId, onboardingId), _ => new ScanSession + { + UserID = userId, + OnboardingID = onboardingId + }); + session.Status = ManuscriptScanStatuses.Failed; + session.Message = Clean(message, "The scan could not finish. Try reconnecting the Word Companion."); + session.UpdatedUtc = DateTime.UtcNow; + return Task.FromResult(ToState(session)); + } + + public Task> FailRunningForUserAsync(int userId, string message) + { + var failed = new List(); + foreach (var pair in sessions) + { + if (pair.Key.UserId != userId || !string.Equals(pair.Value.Status, ManuscriptScanStatuses.Running, StringComparison.Ordinal)) + { + continue; + } + + pair.Value.Status = ManuscriptScanStatuses.Failed; + pair.Value.Message = Clean(message, "The Word Companion disconnected before the scan finished. Reopen Word and try again."); + pair.Value.UpdatedUtc = DateTime.UtcNow; + failed.Add(ToState(pair.Value)); + } + + return Task.FromResult>(failed); + } + + public Task GetStateAsync(int userId, int onboardingId) + => Task.FromResult(sessions.TryGetValue((userId, onboardingId), out var session) + ? ToState(session) + : new ManuscriptScanState()); + + public Task GetLatestPreviewAsync(int userId, int onboardingId) + { + if (!sessions.TryGetValue((userId, onboardingId), out var session) || !session.PreviewID.HasValue) + { + return Task.FromResult(null); + } + + return GetPreviewAsync(userId, session.PreviewID.Value); + } + + public Task GetPreviewAsync(int userId, Guid previewId) + => Task.FromResult(previews.TryGetValue(previewId, out var preview) && preview.UserID == userId ? preview : null); + + private static ManuscriptScanState ToState(ScanSession session) + => new() + { + Status = session.Status, + Message = session.Message, + PercentComplete = session.PercentComplete, + ChapterCount = session.ChapterCount, + SceneCount = session.SceneCount, + CharacterCandidateCount = session.CharacterCandidateCount, + TotalWordCount = session.TotalWordCount, + PreviewID = session.PreviewID, + UpdatedUtc = session.UpdatedUtc + }; + + private static string Clean(string? value, string fallback) + { + var cleaned = (value ?? string.Empty).Trim(); + return string.IsNullOrWhiteSpace(cleaned) ? fallback : cleaned; + } + + private sealed class ScanSession + { + public int UserID { get; init; } + public int OnboardingID { get; init; } + public int ProjectID { get; init; } + public int BookID { get; init; } + public string Status { get; set; } = ManuscriptScanStatuses.NotStarted; + public string Message { get; set; } = string.Empty; + public int? PercentComplete { get; set; } + public int ChapterCount { get; set; } + public int SceneCount { get; set; } + public int CharacterCandidateCount { get; set; } + public int TotalWordCount { get; set; } + public Guid? PreviewID { get; set; } + public DateTime? UpdatedUtc { get; set; } + } +} + +file static class ManuscriptScanPreviewExtensions +{ + public static ManuscriptScanPreview WithUser(this ManuscriptScanPreview preview, int userId) + => new() + { + PreviewID = preview.PreviewID == Guid.Empty ? Guid.NewGuid() : preview.PreviewID, + UserID = userId, + OnboardingID = preview.OnboardingID, + ProjectID = preview.ProjectID, + BookID = preview.BookID, + Source = preview.Source, + DocumentTitle = preview.DocumentTitle, + CompanionDocumentIdentifier = preview.CompanionDocumentIdentifier, + TotalWordCount = preview.TotalWordCount, + ChapterCount = preview.ChapterCount, + SceneCount = preview.SceneCount, + CharacterCandidateCount = preview.CharacterCandidateCount, + CreatedUtc = preview.CreatedUtc == default ? DateTime.UtcNow : preview.CreatedUtc, + Chapters = preview.Chapters, + Scenes = preview.Scenes, + CharacterCandidates = preview.CharacterCandidates + }; +} diff --git a/PlotLine/Services/OnboardingService.cs b/PlotLine/Services/OnboardingService.cs index 39896e9..f1506ab 100644 --- a/PlotLine/Services/OnboardingService.cs +++ b/PlotLine/Services/OnboardingService.cs @@ -8,6 +8,7 @@ public interface IOnboardingService { Task GetOrCreateAsync(); Task GetWizardAsync(); + Task GetScanReviewAsync(Guid? previewId = null); Task ShouldShowOnboardingAsync(); Task GetDashboardNudgeAsync(); Task ShouldShowWordCompanionHeaderAsync(); @@ -27,6 +28,7 @@ public sealed class OnboardingService( IBookService bookService, IProjectActivityService activity, IWordCompanionPresenceService companionPresence, + IManuscriptScanPreviewStore scanPreviews, ICurrentUserService currentUser) : IOnboardingService { private static readonly HashSet WritingJourneys = new(StringComparer.Ordinal) @@ -64,6 +66,7 @@ public sealed class OnboardingService( ? await books.GetAsync(state.BookID.Value) : null; var presence = await companionPresence.GetStatusAsync(RequireUserId()); + var scanState = await scanPreviews.GetStateAsync(RequireUserId(), state.UserOnboardingStateID); return new OnboardingWizardViewModel { @@ -76,6 +79,7 @@ public sealed class OnboardingService( SelectedProjectName = selectedProject?.ProjectName, SelectedBookTitle = selectedBook is null ? null : BookOptionTitle(selectedBook), CompanionPresence = ToPresenceViewModel(presence), + ScanState = ToScanStateViewModel(scanState), ProjectOptions = projectOptions, BookOptions = bookOptions, ProjectForm = new ProjectOnboardingForm @@ -97,6 +101,64 @@ public sealed class OnboardingService( }; } + public async Task GetScanReviewAsync(Guid? previewId = null) + { + var userId = RequireUserId(); + var state = await onboarding.GetAsync(userId); + if (state is null || !IsMicrosoftWordState(state)) + { + return null; + } + + var preview = previewId.HasValue + ? await scanPreviews.GetPreviewAsync(userId, previewId.Value) + : await scanPreviews.GetLatestPreviewAsync(userId, state.UserOnboardingStateID); + if (preview is null + || preview.OnboardingID != state.UserOnboardingStateID + || preview.ProjectID != state.ProjectID + || preview.BookID != state.BookID) + { + return null; + } + + var project = await projects.GetForUserAsync(preview.ProjectID, userId); + var book = await books.GetAsync(preview.BookID); + if (project is null || book is null || book.ProjectID != project.ProjectID) + { + return null; + } + + return new ManuscriptScanReviewViewModel + { + PreviewID = preview.PreviewID, + DocumentTitle = string.IsNullOrWhiteSpace(preview.DocumentTitle) ? "Word manuscript" : preview.DocumentTitle!, + SelectedProjectName = project.ProjectName, + SelectedBookTitle = BookOptionTitle(book), + TotalWordCount = preview.TotalWordCount, + ChapterCount = preview.ChapterCount, + SceneCount = preview.SceneCount, + CharacterCandidateCount = preview.CharacterCandidateCount, + Chapters = preview.Chapters + .OrderBy(chapter => chapter.ChapterNumber) + .Select(chapter => new ManuscriptScanReviewChapterViewModel + { + TemporaryChapterKey = chapter.TemporaryChapterKey, + ChapterNumber = chapter.ChapterNumber, + Title = chapter.Title, + WordCount = chapter.WordCount, + Scenes = preview.Scenes + .Where(scene => string.Equals(scene.TemporaryChapterKey, chapter.TemporaryChapterKey, StringComparison.Ordinal)) + .OrderBy(scene => scene.SceneNumberWithinChapter) + .ToList() + }) + .ToList(), + CharacterCandidates = preview.CharacterCandidates + .OrderByDescending(candidate => candidate.MentionCount) + .ThenBy(candidate => candidate.Name) + .ToList() + }; + } + public async Task ShouldShowOnboardingAsync() { if (!currentUser.UserId.HasValue) @@ -425,12 +487,26 @@ public sealed class OnboardingService( => new() { IsConnected = status.IsConnected, + DocumentOpen = status.DocumentOpen, Status = status.Status, CompanionVersion = status.CompanionVersion, CurrentDocumentName = status.CurrentDocumentName, LastHeartbeatUtc = status.LastHeartbeatUtc }; + private static ManuscriptScanStateViewModel ToScanStateViewModel(ManuscriptScanState state) + => new() + { + Status = state.Status, + Message = state.Message, + PercentComplete = state.PercentComplete, + ChapterCount = state.ChapterCount, + SceneCount = state.SceneCount, + CharacterCandidateCount = state.CharacterCandidateCount, + TotalWordCount = state.TotalWordCount, + PreviewID = state.PreviewID + }; + private static string? CleanOptional(string? value) { var cleaned = Clean(value); diff --git a/PlotLine/Services/WordCompanionPresenceMonitor.cs b/PlotLine/Services/WordCompanionPresenceMonitor.cs index 31f2345..f4b559b 100644 --- a/PlotLine/Services/WordCompanionPresenceMonitor.cs +++ b/PlotLine/Services/WordCompanionPresenceMonitor.cs @@ -5,6 +5,7 @@ namespace PlotLine.Services; public sealed class WordCompanionPresenceMonitor( IWordCompanionPresenceService presence, + IManuscriptScanPreviewStore scanStore, IHubContext hub) : BackgroundService { protected override async Task ExecuteAsync(CancellationToken stoppingToken) @@ -20,6 +21,15 @@ public sealed class WordCompanionPresenceMonitor( await hub.Clients .Group(PresenceGroup(status.UserID)) .SendAsync("WordCompanionPresenceChanged", status, stoppingToken); + var failedScans = await scanStore.FailRunningForUserAsync( + status.UserID, + "The Word Companion disconnected before the scan finished. Reopen Word and try again."); + foreach (var failedScan in failedScans) + { + await hub.Clients + .Group(PresenceGroup(status.UserID)) + .SendAsync("OnboardingScanFailed", failedScan, stoppingToken); + } } } } diff --git a/PlotLine/Services/WordCompanionPresenceService.cs b/PlotLine/Services/WordCompanionPresenceService.cs index 2115772..aaf6ff6 100644 --- a/PlotLine/Services/WordCompanionPresenceService.cs +++ b/PlotLine/Services/WordCompanionPresenceService.cs @@ -11,6 +11,7 @@ public interface IWordCompanionPresenceService Task GetStatusAsync(int userId); Task IsConnectedAsync(int userId); Task> MarkStaleOfflineAsync(); + Task GetCompanionConnectionIdAsync(int userId); } public sealed class WordCompanionPresenceService : IWordCompanionPresenceService @@ -99,6 +100,19 @@ public sealed class WordCompanionPresenceService : IWordCompanionPresenceService public async Task IsConnectedAsync(int userId) => (await GetStatusAsync(userId)).IsConnected; + public Task GetCompanionConnectionIdAsync(int userId) + { + if (!recordsByUser.TryGetValue(userId, out var record)) + { + return Task.FromResult(null); + } + + var status = ToStatus(record, DateTime.UtcNow); + return Task.FromResult(status.IsConnected && !string.IsNullOrWhiteSpace(record.ConnectionID) + ? record.ConnectionID + : null); + } + public Task> MarkStaleOfflineAsync() { var now = DateTime.UtcNow; diff --git a/PlotLine/ViewModels/OnboardingViewModels.cs b/PlotLine/ViewModels/OnboardingViewModels.cs index 1bf234d..faa7d99 100644 --- a/PlotLine/ViewModels/OnboardingViewModels.cs +++ b/PlotLine/ViewModels/OnboardingViewModels.cs @@ -16,6 +16,7 @@ public sealed class OnboardingWizardViewModel public string? SelectedBookTitle { get; set; } public bool IsMicrosoftWordPath => string.Equals(WritingSoftware, WritingSoftwareValues.MicrosoftWord, StringComparison.Ordinal); public CompanionPresenceViewModel CompanionPresence { get; set; } = new(); + public ManuscriptScanStateViewModel ScanState { get; set; } = new(); public IReadOnlyList Options { get; set; } = []; public IReadOnlyList ProjectOptions { get; set; } = []; public IReadOnlyList BookOptions { get; set; } = []; @@ -106,8 +107,47 @@ public sealed class OnboardingNudgeViewModel public sealed class CompanionPresenceViewModel { public bool IsConnected { get; init; } + public bool DocumentOpen { get; init; } public string Status { get; init; } = "Offline"; public string? CompanionVersion { get; init; } public string? CurrentDocumentName { get; init; } public DateTime? LastHeartbeatUtc { get; init; } } + +public sealed class ManuscriptScanStateViewModel +{ + public string Status { get; init; } = ManuscriptScanStatuses.NotStarted; + public string Message { get; init; } = string.Empty; + public int? PercentComplete { get; init; } + public int ChapterCount { get; init; } + public int SceneCount { get; init; } + public int CharacterCandidateCount { get; init; } + public int TotalWordCount { get; init; } + public Guid? PreviewID { get; init; } + public bool IsRunning => string.Equals(Status, ManuscriptScanStatuses.Running, StringComparison.Ordinal); + public bool IsComplete => string.Equals(Status, ManuscriptScanStatuses.Complete, StringComparison.Ordinal); + public bool IsFailed => string.Equals(Status, ManuscriptScanStatuses.Failed, StringComparison.Ordinal); +} + +public sealed class ManuscriptScanReviewViewModel +{ + public Guid PreviewID { get; init; } + public string DocumentTitle { get; init; } = "Word manuscript"; + public string SelectedProjectName { get; init; } = string.Empty; + public string SelectedBookTitle { get; init; } = string.Empty; + public int TotalWordCount { get; init; } + public int ChapterCount { get; init; } + public int SceneCount { get; init; } + public int CharacterCandidateCount { get; init; } + public IReadOnlyList Chapters { get; init; } = []; + public IReadOnlyList CharacterCandidates { get; init; } = []; +} + +public sealed class ManuscriptScanReviewChapterViewModel +{ + public string TemporaryChapterKey { get; init; } = string.Empty; + public int ChapterNumber { get; init; } + public string Title { get; init; } = string.Empty; + public int WordCount { get; init; } + public IReadOnlyList Scenes { get; init; } = []; +} diff --git a/PlotLine/Views/Onboarding/Index.cshtml b/PlotLine/Views/Onboarding/Index.cshtml index 74c2f2e..b2ea0e8 100644 --- a/PlotLine/Views/Onboarding/Index.cshtml +++ b/PlotLine/Views/Onboarding/Index.cshtml @@ -258,9 +258,7 @@

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.") + @CompanionScanMessage(Model.CompanionPresence, Model.ScanState)

@@ -272,6 +270,50 @@
@(Model.CompanionPresence.CurrentDocumentName ?? "No document reported yet")
+
+
+ @ScanMessage(Model.ScanState) + @ScanPercent(Model.ScanState) +
+ +
+
+ Chapters + @Model.ScanState.ChapterCount +
+
+ Scenes + @Model.ScanState.SceneCount +
+
+ Characters + @Model.ScanState.CharacterCandidateCount +
+
+ Words + @Model.ScanState.TotalWordCount.ToString("N0") +
+
+
+ + + Review scan + +
+
} @@ -332,4 +374,56 @@ WritingSoftwareValues.Other => "Other", _ => "Not set" }; + + private static bool CanStartScan(CompanionPresenceViewModel presence, ManuscriptScanStateViewModel scan) + => presence.IsConnected && presence.DocumentOpen && !scan.IsRunning; + + private static string CompanionScanMessage(CompanionPresenceViewModel presence, ManuscriptScanStateViewModel scan) + { + if (!presence.IsConnected) + { + return "Open Word and start the Companion. This page will update automatically."; + } + + if (!presence.DocumentOpen) + { + return "Open your manuscript in Word, then the scan button will appear."; + } + + if (scan.IsComplete) + { + return "Scan complete. Review the structure before anything is imported."; + } + + if (scan.IsRunning) + { + return "The Companion is scanning your manuscript now."; + } + + if (scan.IsFailed) + { + return "The scan needs another try. Your story data has not been changed."; + } + + return "Your Companion is connected. Scan your manuscript when you are ready."; + } + + private static string ScanMessage(ManuscriptScanStateViewModel scan) + { + if (!string.IsNullOrWhiteSpace(scan.Message)) + { + return scan.Message; + } + + return scan.Status switch + { + ManuscriptScanStatuses.Complete => "We found:", + ManuscriptScanStatuses.Running => "Preparing document scan", + ManuscriptScanStatuses.Failed => "The scan could not finish.", + _ => "Ready to scan" + }; + } + + private static string ScanPercent(ManuscriptScanStateViewModel scan) + => scan.PercentComplete.HasValue ? $"{scan.PercentComplete.Value}%" : string.Empty; } diff --git a/PlotLine/Views/Onboarding/ScanReview.cshtml b/PlotLine/Views/Onboarding/ScanReview.cshtml new file mode 100644 index 0000000..f67e015 --- /dev/null +++ b/PlotLine/Views/Onboarding/ScanReview.cshtml @@ -0,0 +1,102 @@ +@model ManuscriptScanReviewViewModel +@{ + ViewData["Title"] = "Review manuscript scan"; +} + +
+
+
+

@Model.SelectedBookTitle

+

Review manuscript scan

+

@Model.DocumentTitle

+
+ +
+
+ Chapters + @Model.ChapterCount +
+
+ Scenes + @Model.SceneCount +
+
+ Characters + @Model.CharacterCandidateCount +
+
+ Words + @Model.TotalWordCount.ToString("N0") +
+
+ +
+
+

Chapters and scenes

+ @if (!Model.Chapters.Any()) + { +

No chapters were detected. Make sure your chapter headings use Heading 1, or add clear chapter headings.

+ } + else + { +
+ @foreach (var chapter in Model.Chapters) + { +
+
+ Chapter @chapter.ChapterNumber + @chapter.Title + @chapter.WordCount.ToString("N0") words / @chapter.Scenes.Count scene@(chapter.Scenes.Count == 1 ? string.Empty : "s") +
+ @if (chapter.Scenes.Any()) + { +
    + @foreach (var scene in chapter.Scenes) + { +
  1. + @(scene.Title ?? $"Scene {scene.SceneNumberWithinChapter}") + @scene.WordCount.ToString("N0") words + @if (!string.IsNullOrWhiteSpace(scene.OpeningTextPreview)) + { +

    @scene.OpeningTextPreview

    + } +
  2. + } +
+ } + else + { +

No scene breaks were detected. PlotDirector will treat this chapter as one scene for now.

+ } +
+ } +
+ } +
+ + +
+ + +
+
diff --git a/PlotLine/wwwroot/css/onboarding.css b/PlotLine/wwwroot/css/onboarding.css index b7b601b..4e0f2df 100644 --- a/PlotLine/wwwroot/css/onboarding.css +++ b/PlotLine/wwwroot/css/onboarding.css @@ -243,6 +243,156 @@ overflow-wrap: anywhere; } +.onboarding-scan-panel { + display: grid; + gap: .85rem; + margin-top: 1rem; + border-top: 1px solid rgba(31, 42, 68, .1); + padding-top: 1rem; +} + +.onboarding-scan-status { + display: flex; + justify-content: space-between; + gap: .75rem; + color: var(--bs-secondary-color); + font-weight: 700; +} + +.onboarding-scan-progress { + height: .5rem; + overflow: hidden; + border-radius: 999px; + background: rgba(31, 42, 68, .12); +} + +.onboarding-scan-progress span { + display: block; + width: 0; + height: 100%; + border-radius: inherit; + background: #2f6f63; + transition: width .18s ease; +} + +.onboarding-scan-counts { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: .65rem; +} + +.onboarding-scan-counts div { + min-width: 0; + border: 1px solid rgba(31, 42, 68, .1); + border-radius: 8px; + padding: .75rem; + background: rgba(255, 255, 255, .62); +} + +.onboarding-scan-counts span { + display: block; + color: var(--bs-secondary-color); + font-size: .78rem; + font-weight: 800; + text-transform: uppercase; +} + +.onboarding-scan-counts strong { + display: block; + margin-top: .15rem; + font-size: 1.2rem; +} + +.onboarding-scan-actions { + display: flex; + flex-wrap: wrap; + gap: .65rem; +} + +.onboarding-review-panel { + width: min(1120px, 100%); +} + +.onboarding-review-counts { + margin-bottom: 1.25rem; +} + +.onboarding-review-grid { + display: grid; + grid-template-columns: minmax(0, 1.7fr) minmax(280px, .8fr); + gap: 1rem; +} + +.onboarding-review-section { + display: grid; + align-content: start; + gap: .8rem; +} + +.onboarding-review-section h2 { + margin: 0; + font-size: 1.2rem; +} + +.onboarding-review-chapters { + display: grid; + gap: .85rem; +} + +.onboarding-review-chapter { + border: 1px solid rgba(31, 42, 68, .12); + border-radius: 8px; + padding: 1rem; + background: rgba(255, 255, 255, .58); +} + +.onboarding-review-chapter header { + display: grid; + gap: .2rem; + margin-bottom: .75rem; +} + +.onboarding-review-chapter header span, +.onboarding-review-chapter header small, +.onboarding-review-chapter li span { + color: var(--bs-secondary-color); + font-weight: 700; +} + +.onboarding-review-chapter ol, +.onboarding-character-candidates { + display: grid; + gap: .65rem; + margin: 0; + padding: 0; + list-style: none; +} + +.onboarding-review-chapter li, +.onboarding-character-candidates li { + border: 1px solid rgba(31, 42, 68, .1); + border-radius: 8px; + padding: .75rem; + background: rgba(255, 255, 255, .64); +} + +.onboarding-review-chapter li { + display: grid; + gap: .2rem; +} + +.onboarding-review-chapter li p { + margin: .2rem 0 0; + color: var(--bs-secondary-color); +} + +.onboarding-character-candidates li { + display: flex; + align-items: center; + justify-content: space-between; + gap: .75rem; +} + .onboarding-summary div { display: flex; align-items: center; @@ -333,6 +483,7 @@ .onboarding-dashboard-nudge, .onboarding-companion-card, + .onboarding-review-grid, .onboarding-summary div { align-items: stretch; grid-template-columns: 1fr; @@ -342,4 +493,8 @@ .onboarding-companion-details { grid-template-columns: 1fr; } + + .onboarding-scan-counts { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } } diff --git a/PlotLine/wwwroot/js/word-companion-host.js b/PlotLine/wwwroot/js/word-companion-host.js index c252fe1..41907bb 100644 --- a/PlotLine/wwwroot/js/word-companion-host.js +++ b/PlotLine/wwwroot/js/word-companion-host.js @@ -1251,6 +1251,9 @@ .build(); companionPresenceConnection.onreconnected(refreshCompanionPresence); + companionPresenceConnection.on("ScanCurrentDocument", (command) => { + void runOnboardingDocumentScan(command); + }); companionPresenceConnection.onclose(() => { if (companionPresenceHeartbeatTimer) { window.clearInterval(companionPresenceHeartbeatTimer); @@ -1890,6 +1893,247 @@ }; }; + const properNameStopWords = new Set([ + "Chapter", "Scene", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday", + "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", + "November", "December", "The", "A", "An", "And", "But", "Or", "If", "Then", "When", "Where", + "This", "That", "These", "Those", "He", "She", "They", "We", "I", "You", "It", "His", "Her", + "Their", "Our", "Your", "My", "Word", "PlotDirector" + ]); + + const candidateCharacterNames = (text) => { + const counts = new Map(); + const matches = String(text || "").match(/\b[A-Z][a-z]+(?:\s+[A-Z][a-z]+){0,2}\b/g) || []; + for (const match of matches) { + const name = match.trim(); + const parts = name.split(/\s+/); + if (parts.some((part) => properNameStopWords.has(part)) || name.length < 3) { + continue; + } + + counts.set(name, (counts.get(name) || 0) + 1); + } + + return [...counts.entries()] + .filter(([, count]) => count >= 2) + .sort((left, right) => right[1] - left[1] || left[0].localeCompare(right[0])) + .slice(0, 40) + .map(([name, count], index) => ({ + temporaryCharacterKey: `character-${index + 1}`, + name, + mentionCount: count, + suggestedImportance: null + })); + }; + + const buildOnboardingScanPreview = (paragraphs, command) => { + const chapters = []; + const scenes = []; + const documentText = []; + let chapter = null; + let scene = null; + let totalWordCount = 0; + let detectedHeadingChapters = 0; + + const finishScene = () => { + scene = null; + }; + + const startChapter = (paragraph, title, isOpening = false) => { + finishScene(); + chapter = { + temporaryChapterKey: `chapter-${chapters.length + 1}`, + chapterNumber: chapters.length + 1, + title, + wordCount: 0, + startPosition: paragraph?.index ?? null, + existingChapterID: isOpening ? null : paragraphAnchorId(paragraph, "PD-CHAPTER") + }; + chapters.push(chapter); + startScene(paragraph, null); + return chapter; + }; + + const startScene = (paragraph, title = null) => { + if (!chapter) { + return null; + } + + finishScene(); + scene = { + temporarySceneKey: `scene-${scenes.length + 1}`, + temporaryChapterKey: chapter.temporaryChapterKey, + sceneNumberWithinChapter: scenes.filter((item) => item.temporaryChapterKey === chapter.temporaryChapterKey).length + 1, + title, + wordCount: 0, + openingTextPreview: "", + startPosition: paragraph?.index ?? null, + existingSceneID: paragraph ? paragraphAnchorId(paragraph, "PD-SCENE") : null + }; + scenes.push(scene); + return scene; + }; + + const addWords = (text, words) => { + if (!chapter || !scene || words <= 0) { + return; + } + + chapter.wordCount += words; + scene.wordCount += words; + if (!scene.openingTextPreview && text && !sceneSeparatorTexts.has(text)) { + scene.openingTextPreview = text.length > 180 ? `${text.slice(0, 177)}...` : text; + } + }; + + paragraphs.forEach((paragraph, index) => { + paragraph.index = index; + const text = String(paragraph.text || "").trim(); + if (!text) { + return; + } + + documentText.push(text); + const words = countWords(text); + + if (isBuiltInHeading(paragraph, 1)) { + detectedHeadingChapters += 1; + startChapter(paragraph, text); + totalWordCount += words; + chapter.wordCount += words; + return; + } + + if (!chapter) { + startChapter(paragraph, "Opening pages", true); + } + + if (isBuiltInHeading(paragraph, 2)) { + startScene(paragraph, text); + totalWordCount += words; + addWords(text, words); + return; + } + + if (sceneSeparatorTexts.has(text)) { + startScene(paragraph, null); + return; + } + + totalWordCount += words; + addWords(text, words); + }); + + if (totalWordCount === 0) { + throw new Error("The document appears to be empty. Add manuscript text, then try again."); + } + + if (detectedHeadingChapters === 0) { + throw new Error("No chapters were detected. Make sure your chapter headings use Heading 1, or add clear chapter headings."); + } + + const characterCandidates = candidateCharacterNames(documentText.join("\n")); + return { + previewID: "00000000-0000-0000-0000-000000000000", + userID: command.userID || command.UserID || 0, + onboardingID: command.onboardingID || command.OnboardingID || 0, + projectID: command.projectID || command.ProjectID || 0, + bookID: command.bookID || command.BookID || 0, + source: "WordCompanion", + documentTitle: currentDocumentName() || "Word manuscript", + companionDocumentIdentifier: currentDocumentGuid(), + totalWordCount, + chapterCount: chapters.length, + sceneCount: scenes.length, + characterCandidateCount: characterCandidates.length, + createdUtc: new Date().toISOString(), + chapters, + scenes, + characterCandidates + }; + }; + + const scanCommandValue = (command, camel, pascal) => command?.[camel] ?? command?.[pascal] ?? null; + + const reportOnboardingScanProgress = async (command, message, percentComplete, counts = {}) => { + if (!companionPresenceConnection || companionPresenceConnection.state !== signalR.HubConnectionState.Connected) { + return; + } + + await companionPresenceConnection.invoke("ReportOnboardingScanProgress", { + userID: scanCommandValue(command, "userID", "UserID"), + onboardingID: scanCommandValue(command, "onboardingID", "OnboardingID"), + message, + percentComplete, + chapterCount: counts.chapterCount || 0, + sceneCount: counts.sceneCount || 0, + characterCandidateCount: counts.characterCandidateCount || 0, + totalWordCount: counts.totalWordCount || 0 + }); + }; + + const failOnboardingScan = async (command, message) => { + if (!companionPresenceConnection || companionPresenceConnection.state !== signalR.HubConnectionState.Connected) { + return; + } + + await companionPresenceConnection.invoke("FailOnboardingScan", { + userID: scanCommandValue(command, "userID", "UserID"), + onboardingID: scanCommandValue(command, "onboardingID", "OnboardingID"), + message + }); + }; + + const runOnboardingDocumentScan = async (command) => { + if (!wordHostAvailable || !window.Word) { + await failOnboardingScan(command, "Open your manuscript in Microsoft Word, then try the scan again."); + return; + } + + try { + await reportOnboardingScanProgress(command, "Preparing document scan", 5); + const paragraphs = await window.Word.run(async (context) => { + const bodyParagraphs = context.document.body.paragraphs; + bodyParagraphs.load("items/text,items/style,items/styleBuiltIn"); + await context.sync(); + + await reportOnboardingScanProgress(command, "Detecting chapters", 20); + for (const paragraph of bodyParagraphs.items) { + if (isBuiltInHeading(paragraph, 1) || isBuiltInHeading(paragraph, 2)) { + paragraph.contentControls.load("items/tag,title"); + } + } + await context.sync(); + return bodyParagraphs.items; + }); + + const preview = buildOnboardingScanPreview(paragraphs, command); + await reportOnboardingScanProgress(command, "Detecting scene breaks", 50, preview); + await reportOnboardingScanProgress(command, `Scene ${preview.sceneCount} of ${preview.sceneCount} scanned`, 70, preview); + await reportOnboardingScanProgress(command, "Finding character candidates", 85, preview); + await reportOnboardingScanProgress(command, "Preparing preview", 95, preview); + await companionPresenceConnection.invoke("CompleteOnboardingScan", preview); + } catch (error) { + console.error("Unable to complete onboarding scan.", error); + await failOnboardingScan(command, friendlyScanError(error)); + } + }; + + const friendlyScanError = (error) => { + const message = errorText(error); + if (/No chapters were detected/i.test(message)) { + return "No chapters were detected. Make sure your chapter headings use Heading 1, or add clear chapter headings."; + } + if (/empty/i.test(message)) { + return "The document appears to be empty. Add manuscript text, then try again."; + } + if (/network|disconnect|connection/i.test(message)) { + return "The Word Companion disconnected before the scan finished. Reopen Word and try again."; + } + + return `The scan could not finish: ${message}`; + }; + const paragraphMatchesSnapshot = (snapshot, paragraph) => { return String(snapshot?.text || "").trim() === String(paragraph?.text || "").trim() && normalizedStyleName(snapshot) === normalizedStyleName(paragraph); diff --git a/PlotLine/wwwroot/js/word-companion-host.min.js b/PlotLine/wwwroot/js/word-companion-host.min.js index 73a51c3..56f8fa0 100644 --- a/PlotLine/wwwroot/js/word-companion-host.min.js +++ b/PlotLine/wwwroot/js/word-companion-host.min.js @@ -1,4 +1,4 @@ -(()=>{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? +(()=>{const s={projectId:"plotdirector.word.projectId",bookId:"plotdirector.word.bookId",activeTab:"plotdirector.word.activeTab"},bi={documentGuid:"plotdirector.word.documentGuid",bookId:"plotdirector.word.boundBookId",projectId:"plotdirector.word.boundProjectId",bindingVersion:"plotdirector.word.bindingVersion"},vl=new Set(["scene","review","maintenance","diagnostics"]),gs=document.querySelector(".word-companion-shell"),yp=document.querySelector(".word-companion-tabs"),nh=[...document.querySelectorAll("[data-tab-button]")],yl=[...document.querySelectorAll("[data-tab-panel]")],pp=document.querySelector("[data-linked-manuscript-card]"),wp=document.querySelector("[data-linked-project-title]"),bp=document.querySelector("[data-linked-book-title]"),kp=document.querySelector("[data-first-run-wizard]"),et=document.querySelector("[data-first-run-project-select]"),d=document.querySelector("[data-first-run-book-select]"),df=document.querySelector("[data-first-run-new-book-title]"),fo=document.querySelector("[data-first-run-create-book]"),sr=document.querySelector("[data-first-run-scan]"),dp=document.querySelector("[data-first-run-cancel]"),gp=document.querySelector("[data-first-run-status]"),hr=document.querySelector("[data-first-run-summary]"),pu=document.querySelector("[data-first-run-character-section]"),wu=document.querySelector("[data-first-run-character-status]"),ki=document.querySelector("[data-first-run-character-candidates]"),bu=document.querySelector("[data-first-run-character-actions]"),eo=document.querySelector("[data-first-run-create-characters]"),pl=document.querySelector("[data-first-run-skip-characters]"),wl=document.querySelector("[data-first-run-continue]"),th=document.querySelector("[data-first-run-import-actions]"),ih=document.querySelector("[data-first-run-import]"),nw=document.querySelector("[data-first-run-import-cancel]"),ku=document.querySelector("[data-first-run-replace-dialog]"),bl=document.querySelector("[data-first-run-replace-confirm]"),kl=document.querySelector("[data-first-run-replace-cancel]"),du=document.querySelector("[data-unlink-word-document-dialog]"),dl=document.querySelector("[data-unlink-word-document-confirm]"),gl=document.querySelector("[data-unlink-word-document-cancel]"),na=document.querySelector("[data-office-status]"),ta=document.querySelector("[data-connection-status]"),ia=document.querySelector("[data-summary-connection]"),ra=document.querySelector("[data-summary-project]"),ua=document.querySelector("[data-summary-book]"),p=document.querySelector("[data-project-select]"),ot=document.querySelector("[data-book-select]"),tw=document.querySelector("[data-runtime-book-selectors]"),fa=document.querySelector("[data-project-message]"),ea=document.querySelector("[data-book-message]"),oo=document.querySelector("[data-refresh-current-scene]"),nu=document.querySelector("[data-refresh-document]"),oa=document.querySelector("[data-current-chapter]"),sa=document.querySelector("[data-current-scene]"),ha=document.querySelector("[data-current-word-count]"),ca=document.querySelector("[data-scene-tracking-status]"),la=document.querySelector("[data-document-message]"),aa=document.querySelector("[data-sync-note]"),gf=document.querySelector("[data-document-outline]"),cr=document.querySelector("[data-analyse-manuscript-structure]"),rh=document.querySelector("[data-apply-structure-sync]"),iw=document.querySelector("[data-structure-preview-status]"),gu=document.querySelector("[data-structure-preview-results]"),tu=document.querySelector("[data-refresh-plotdirector-scene]"),va=document.querySelector("[data-plotdirector-scene-status]"),rw=document.querySelector("[data-anchor-status]"),uw=document.querySelector("[data-anchor-book-id]"),fw=document.querySelector("[data-anchor-chapter-id]"),ew=document.querySelector("[data-anchor-scene-id]"),ai=document.querySelector("[data-attach-plotdirector-ids]"),lr=document.querySelector("[data-unlink-word-document]"),ya=document.querySelector("[data-plotdirector-scene-details]"),pa=document.querySelector("[data-plotdirector-scene-title]"),wa=document.querySelector("[data-plotdirector-revision-status]"),ba=document.querySelector("[data-plotdirector-estimated-words]"),uh=document.querySelector("[data-plotdirector-actual-words]"),ka=document.querySelector("[data-plotdirector-blocked-status]"),fh=document.querySelector("[data-plotdirector-blocked-reason-row]"),eh=document.querySelector("[data-plotdirector-blocked-reason]"),da=document.querySelector("[data-runtime-last-sync]"),ar=document.querySelector("[data-sync-scene-progress]"),ow=document.querySelector("[data-sync-word-count]"),oh=document.querySelector("[data-sync-plotdirector-count]"),ga=document.querySelector("[data-sync-last-synced]"),sw=document.querySelector("[data-sync-status]"),nv=document.querySelector("[data-writing-brief-block]"),tv=document.querySelector("[data-writing-brief]"),iv=document.querySelector("[data-plotdirector-link-groups]"),hw=document.querySelector("[data-character-review-summary]"),cw=document.querySelector("[data-asset-review-summary]"),lw=document.querySelector("[data-location-review-summary]"),ne=document.querySelector("[data-character-chips]"),te=document.querySelector("[data-asset-chips]"),ptt=document.querySelector("[data-location-chips]"),vr=document.querySelector("[data-primary-location-select]"),di=document.querySelector("[data-save-scene-links]"),aw=document.querySelector("[data-scene-links-status]"),rv=document.querySelector("[data-links-editing-scene]"),uv=document.querySelector("[data-chapter-create-link]"),vw=document.querySelector("[data-chapter-create-link-chapter]"),yt=document.querySelector("[data-create-plotdirector-chapter]"),pt=document.querySelector("[data-link-existing-chapter]"),yw=document.querySelector("[data-chapter-create-link-status]"),fv=document.querySelector("[data-scene-create-link]"),pw=document.querySelector("[data-create-link-chapter]"),ww=document.querySelector("[data-create-link-scene]"),wt=document.querySelector("[data-create-plotdirector-scene]"),bt=document.querySelector("[data-link-existing-scene]"),bw=document.querySelector("[data-create-link-status]"),yr=document.querySelector("[data-link-existing-dialog]"),so=document.querySelector("[data-link-scene-search]"),ho=document.querySelector("[data-link-scene-options]"),oi=document.querySelector("[data-link-selected-scene]"),kw=document.querySelector("[data-link-dialog-cancel]"),sh=document.querySelector("[data-link-dialog-status]"),gi=document.querySelector("[data-create-scene-dialog]"),dw=document.querySelector("[data-create-dialog-scene]"),gw=document.querySelector("[data-create-dialog-chapter]"),nb=document.querySelector("[data-create-dialog-confirm]"),tb=document.querySelector("[data-create-dialog-cancel]"),pr=document.querySelector("[data-link-existing-chapter-dialog]"),co=document.querySelector("[data-link-chapter-search]"),lo=document.querySelector("[data-link-chapter-options]"),si=document.querySelector("[data-link-selected-chapter]"),ib=document.querySelector("[data-link-chapter-dialog-cancel]"),hh=document.querySelector("[data-link-chapter-dialog-status]"),nr=document.querySelector("[data-create-chapter-dialog]"),rb=document.querySelector("[data-create-chapter-dialog-chapter]"),ub=document.querySelector("[data-create-chapter-dialog-book]"),fb=document.querySelector("[data-create-chapter-dialog-confirm]"),eb=document.querySelector("[data-create-chapter-dialog-cancel]"),tr=document.querySelector("[data-apply-structure-sync-dialog]"),ch=document.querySelector("[data-apply-structure-sync-summary]"),ob=document.querySelector("[data-apply-structure-sync-confirm]"),sb=document.querySelector("[data-apply-structure-sync-cancel]"),hb=document.querySelector("[data-resolve-project-id]"),cb=document.querySelector("[data-resolve-project-title]"),lb=document.querySelector("[data-resolve-book-id]"),ab=document.querySelector("[data-resolve-book-title]"),vb=document.querySelector("[data-resolve-detected-chapter]"),yb=document.querySelector("[data-resolve-detected-scene]"),pb=document.querySelector("[data-resolve-request-chapter]"),wb=document.querySelector("[data-resolve-request-scene]"),bb=document.querySelector("[data-resolve-result]"),kb=document.querySelector("[data-resolve-chapter-id]"),db=document.querySelector("[data-resolve-scene-id]"),iu=document.querySelector("[data-run-diagnostics]"),gb=document.querySelector("[data-diagnostic-host]"),nk=document.querySelector("[data-diagnostic-platform]"),tk=document.querySelector("[data-diagnostic-ready]"),ik=document.querySelector("[data-diagnostic-word-api]"),rk=document.querySelector("[data-diagnostic-last-scan]"),uk=document.querySelector("[data-diagnostic-last-error]"),fk=document.querySelector("[data-diagnostic-anchor-type]"),ek=document.querySelector("[data-diagnostic-stored-scene-id]"),ok=document.querySelector("[data-diagnostic-stored-chapter-id]"),sk=document.querySelector("[data-diagnostic-resolution-method]"),hk=document.querySelector("[data-diagnostic-paragraphs]"),ck=document.querySelector("[data-diagnostic-heading1]"),lk=document.querySelector("[data-diagnostic-heading2]"),nf=gs?.dataset.authenticated==="true",lh=Number.parseInt(gs?.dataset.userId||"",10),ak=String(gs?.dataset.companionVersion||"20C").trim();let ir=[],ii=[],kt=!1,it=!1,ao="Unknown",vo="Unknown",w="",rt="",ev=null,ie=null,b=null,g=null,u=null,o=null,re="",rr="Title Match",dt=!1,ah=[],ue=null,yo=null,vh=[],fe=null,po=null,vi=null,ri=null,ur=null,l=null,ee=null,wo=null,wr=null,oe=null,hi=[],bo=!1,tf=!1,se=0,he=!1,yh=!1,ce=!1,ko=null,ph="Manual",st=!1,ru=null,br=!1,ov=0,rf=!1,le=0,ae=null;const ve=1200;let wh=!1,go=!1,ye=null,uf=!1,pe=!1,bh=null,kh=null,we=null,ff=!1,ef=!1,dh=null,gh="",sf=[],nc=null,tc="",ic=null,rc="",uu=[],uc=null,fc="",ec=null,oc="",fu=[],sc=null,hc="",cc="",ns="",lc="",sv=0,nt=null,eu=null,ht={characters:[],assets:[],locations:[],primaryLocationId:null};const i=n=>{const t=Date.now();t-sv<1e3||(sv=t,console.debug(`[Word Companion Runtime] ${n}`))},ci=n=>{console.debug(`[Word Companion Timing] ${n}`)},ac=n=>{na&&(na.textContent=n)},vk=n=>{try{return window.localStorage.getItem(n)||""}catch{return""}},yk=(n,t)=>{try{t?window.localStorage.setItem(n,String(t)):window.localStorage.removeItem(n)}catch{}},hv=n=>n==="links"?"review":n==="structure"?"maintenance":n,cv=(n,t=true)=>{const i=hv(n),r=vl.has(i)&&nh.some(n=>n.dataset.tabButton===i)?i:"scene";for(const n of nh){const t=n.dataset.tabButton===r;n.classList.toggle("is-active",t);n.setAttribute("aria-selected",t?"true":"false")}for(const n of yl){const t=n.dataset.tabPanel===r;n.classList.toggle("is-active",t);n.hidden=!t}t&&yk(s.activeTab,r)},lv=()=>{const n=hv(vk(s.activeTab));cv(vl.has(n)?n:"scene",!1)},be=n=>{ta&&(ta.textContent=n),ia&&(ia.textContent=n)},vc=n=>{fa&&(fa.textContent=n)},hf=n=>{ea&&(ea.textContent=n)},t=n=>{la&&(la.textContent=n)},cf=n=>{va&&(va.textContent=n)},yc=t=>{const i=n=>Object.prototype.hasOwnProperty.call(t,n);i("anchorType")&&n(fk,a(t.anchorType));i("storedSceneId")&&n(ek,a(t.storedSceneId));i("storedChapterId")&&n(ok,a(t.storedChapterId));i("resolutionMethod")&&n(sk,a(t.resolutionMethod))},fr=()=>{const i=h(),f=Number.isInteger(u)&&u>0&&g===u,e=!Number.isInteger(o)||o<=0||b===o,t=f&&e;n(rw,t?"Attached to PlotDirector":"Not attached");n(uw,i?.bookId?String(i.bookId):"-");n(fw,Number.isInteger(o)&&o>0?String(o):"-");n(ew,Number.isInteger(u)&&u>0?String(u):"-");ai&&(r(ai,t&&!dt),ai.disabled=!u||t&&!dt,ai.textContent=g&&!t?"Reattach PlotDirector IDs":"Attach PlotDirector IDs");yc({anchorType:rr||"Title Match",storedSceneId:g,storedChapterId:b,resolutionMethod:re})},lf=(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&&rt===s&&ev===h&&ie===c&&b===l&&g===a;(w=t||"",rt=i||"",ev=Number.isInteger(r)?r:null,ie=Number.isInteger(e)&&e>0?e:null,b=Number.isInteger(u)&&u>0?u:null,g=Number.isInteger(f)&&f>0?f:null,v)||(oa&&(oa.textContent=t||"Not detected"),sa&&(sa.textContent=i||"Not detected"),ha&&(ha.textContent=Number.isInteger(r)?String(r):"-"),n(ow,Number.isInteger(r)?String(r):"-"),fr())},af=()=>{ye&&(window.clearTimeout(ye),ye=null)},r=(n,t)=>{n&&(n.hidden=t)},pc=()=>{r(lr,!ri)},gt=t=>{n(sw,t)},er=t=>{n(iw,t)},vf=(n="Select a project and book to analyse manuscript structure.")=>{if(vi=null,gu){gu.innerHTML="";const n=document.createElement("p");n.textContent="No preview generated.";gu.append(n)}er(n);ts()},pk=(n=vi)=>ay.flatMap(t=>Array.isArray(n?.[t.key])?n[t.key]:[]),ou=(n=vi)=>pk(n).filter(n=>n.category==="couldLink"||n.category==="wouldCreateChapters"||n.category==="wouldCreateScenes"),ts=()=>{rh&&(rh.disabled=ce||ou().length===0)},is=()=>{cr&&(cr.disabled=ce||!ni()||!h()),ts()},wc=()=>{rv&&(rv.textContent=u?`Editing links for: ${ns||rt||"Current scene"}`:"Link a PlotDirector scene first.")},bc=()=>{const n=st||!u,t=n||!o||uf;ar&&(ar.disabled=t);di&&(di.disabled=n);vr&&(vr.disabled=n);for(const t of[ne,te])for(const i of t?[...t.querySelectorAll("input[type='checkbox']")]:[])i.disabled=n},k=t=>{(t==="Live"||t==="Manual")&&(ph=t),n(ca,t||ph||"Manual")},li=(i,r="")=>{st=!!i,st&&(af(),ro()),n(ca,st?"Stale":ph),bc(),r&&(t(r),hu(r))},kr=(n,t=null,i="")=>{u=Number.isInteger(n)&&n>0?n:null,o=Number.isInteger(t)&&t>0?t:null,re=i||"",u!==bh&&(kh=null),u||(af(),ro()),bc(),gt(st?"Refresh Current Scene before syncing.":u?"Ready to sync.":"No resolved PlotDirector scene."),hu(st?"Refresh Current Scene before saving.":u?"Ready to save.":"Link a PlotDirector scene first."),wc(),fr()},av=n=>{oo&&(oo.disabled=n,oo.textContent=n?"Refreshing...":"Refresh Current Scene")},a=n=>n===null||n===undefined||n===""?"-":String(n),wtt=n=>{if(!n)return"-";const t=new Date(n);return Number.isNaN(t.getTime())?"-":t.toLocaleString()},rs=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"})}`},vv=t=>{ur=t||null;const i=ur?.lastSyncUtc||ur?.manuscriptDocument?.lastSyncUtc;n(da,rs(i));n(ga,rs(i));gc()},yf=()=>{l=null,ee=null,wo=null,le+=1,ae=null,kn(),dn(),gn(),vv(null)},su=()=>{ee=null,wo=null},hu=t=>{n(aw,t)},cu=t=>{n(bw,t)},yi=t=>{n(yw,t)},kc=()=>!!h()&&!!w&&!o,dc=()=>!!h()&&!!rt&&Number.isInteger(o)&&o>0,ke=()=>{r(uv,!0),yi(""),yt&&(yt.disabled=!0,yt.textContent="Create Chapter in PlotDirector"),pt&&(pt.disabled=!0,pt.textContent="Link to Existing Chapter")},us=(t="")=>{n(vw,a(w));r(uv,!1);yi(t);const i=kc();yt&&(yt.disabled=!i,yt.textContent="Create Chapter in PlotDirector");pt&&(pt.disabled=!i,pt.textContent="Link to Existing Chapter")},fs=()=>{r(fv,!0),cu(""),wt&&(wt.disabled=!0,wt.textContent="Create Scene in PlotDirector"),bt&&(bt.disabled=!0,bt.textContent="Link to Existing Scene")},de=(t="")=>{n(pw,a(w));n(ww,a(rt));r(fv,!1);cu(t);const i=dc();wt&&(wt.disabled=!i,wt.textContent="Create Scene in PlotDirector");bt&&(bt.disabled=!i,bt.textContent="Link to Existing Scene")},tt=t=>{cf(t),r(ya,!0),r(nv,!0),r(iv,!1),ke(),fs(),re="",ns="",lc="",rr=g?"SceneID Anchor":b?"ChapterID Anchor":"Title Match",dt=!1,ht={characters:[],assets:[],locations:[],primaryLocationId:null},os(ne,[],"character"),os(te,[],"asset"),yv([],null,!1),uy(),kr(null),n(oh,"-"),n(pa,"-"),n(wa,"-"),n(ba,"-"),n(uh,"-"),n(ka,"-"),n(eh,"-"),r(fh,!0),fr(),wc()},es=(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,os=(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(es(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)}}}},yv=(n,t,i=false)=>{if(vr){vr.innerHTML="";const r=document.createElement("option");r.value="";r.textContent="None";vr.append(r);const u=Number.parseInt(t||"",10);for(const t of Array.isArray(n)?n:[]){const n=Number.parseInt(es(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;vr.append(i)}}vr.disabled=!i}},wk=t=>{ns=t?.sceneTitle||rt||"",lc=t?.chapterTitle||w||"",n(pa,a(t?.sceneTitle)),n(wa,a(t?.revisionStatus||t?.revisionStatusName||"Not provided")),n(ba,a(t?.estimatedWords)),n(uh,a(t?.actualWords)),n(oh,a(t?.actualWords)),n(ka,t?.blocked?"Blocked":"Not blocked"),t?.blockedReason?(n(eh,t.blockedReason),r(fh,!1)):(n(eh,"-"),r(fh,!0)),tv&&(tv.textContent=t?.writingBrief||"No writing brief has been added for this scene."),ht={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},os(ne,ht.characters,"character",!!u&&!st),os(te,ht.assets,"asset",!!u&&!st),yv(ht.locations,ht.primaryLocationId,!!u&&!st),uy(),wc(),hu(st?"Refresh Current Scene before saving.":u?"Ready to save.":"No resolved PlotDirector scene."),r(ya,!1),r(nv,!1),r(iv,!1)},lu=n=>String(n||"").trim().replace(/\s+/g," "),ut=(n,t)=>{const i=lu(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=lu(i.replace(u,""));return f||i},vt=t=>{const i=n=>Object.prototype.hasOwnProperty.call(t,n);i("projectId")&&n(hb,a(t.projectId));i("projectTitle")&&n(cb,a(t.projectTitle));i("bookId")&&n(lb,a(t.bookId));i("bookTitle")&&n(ab,a(t.bookTitle));i("detectedChapter")&&n(vb,a(t.detectedChapter));i("detectedScene")&&n(yb,a(t.detectedScene));i("requestChapter")&&n(pb,a(t.requestChapter));i("requestScene")&&n(wb,a(t.requestScene));i("result")&&n(bb,a(t.result));i("chapterId")&&n(kb,a(t.chapterId));i("sceneId")&&n(db,a(t.sceneId))},bk=async(n,t)=>{const i=await at(`/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""},kk=async(n,t)=>{const i=await at(`/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},ss=async n=>{if(wo===n&&Array.isArray(ee?.chapters))return ee.chapters;const t=await at(`/api/word-companion/books/${n}/structure`);return wo=n,ee=t||null,Array.isArray(t?.chapters)?t.chapters:[]},pv=async n=>{const t=await at(`/api/word-companion/books/${n}/chapters`);return Array.isArray(t?.chapters)?t.chapters:[]},wv=n=>{if(Number.isInteger(o)&&o>0){const t=n.find(n=>n.chapterId===o);if(t)return t}const t=ut(w,"chapter").toLocaleLowerCase();return t?n.find(n=>ut(n.title,"chapter").toLocaleLowerCase()===t)||null:null},bv=async()=>{const n=h();return n?wv(await ss(n.bookId)):null},hs=async(n,i,r,u,f)=>{const e=await at(`/api/word-companion/scenes/${i}/companion`);try{e.revisionStatus=await bk(n,i)}catch{e.revisionStatus=""}rr=f||"Title Match";dt=!1;li(!1);cf("Linked to PlotDirector");t("");ke();fs();kr(i,r,u);wk(e);nn(i,r);ks();ds()},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(gb,t.host);i("platform")&&n(nk,t.platform);i("ready")&&n(tk,t.ready);i("wordApi")&&n(ik,t.wordApi);i("lastScan")&&n(rk,t.lastScan);i("lastError")&&n(uk,t.lastError);i("paragraphs")&&n(hk,t.paragraphs);i("heading1")&&n(ck,t.heading1);i("heading2")&&n(lk,t.heading2)},e=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)},kv=async()=>{if(!window.Office||typeof window.Office.onReady!="function")return kt=!1,it=!1,ao="Unavailable",vo="Unavailable",f({host:ao,platform:vo,ready:"No",wordApi:"No"}),null;const n=await window.Office.onReady();return kt=!0,ao=n?.host||"Unknown",vo=n?.platform||"Unknown",it=!!window.Word&&!!window.Office.HostType&&n?.host===window.Office.HostType.Word,f({host:ao,platform:vo,ready:"Yes",wordApi:it?"Yes":"No"}),n},cs=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}},ls=(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}},dr=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},dk=()=>ur?.projectTitle||ni()?.title||"Project",gk=()=>ur?.bookTitle||dr(h())||"Book",gc=()=>{const t=!!ri;r(pp,!t);r(tw,t);t&&(n(wp,dk()),n(bp,gk()))},ni=()=>{const n=Number.parseInt(p?.value||"",10);return ir.find(t=>t.projectId===n)||null},h=()=>{const n=Number.parseInt(ot?.value||"",10);return ii.find(t=>t.bookId===n)||null},gr=()=>{const n=Number.parseInt(et?.value||"",10);return ir.find(t=>t.projectId===n)||null},au=()=>{const n=Number.parseInt(d?.value||"",10);return ii.find(t=>t.bookId===n)||null},nd=()=>{const n=ri?.projectId||gr()?.projectId||ni()?.projectId||cs(s.projectId);return Number.isInteger(n)&&n>0?n:null},td=()=>{const n=ri?.bookId||au()?.bookId||h()?.bookId||cs(s.bookId);return Number.isInteger(n)&&n>0?n:null},dv=()=>{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}},gv=()=>({userID:Number.isInteger(lh)&&lh>0?lh:null,companionVersion:ak,machineName:"",documentOpen:!!it,currentDocumentName:dv(),linkedProjectID:nd(),linkedBookID:td()}),id=async()=>{nt&&nt.state===signalR.HubConnectionState.Connected&&await nt.invoke("CompanionHeartbeat",gv())},pf=()=>{id().catch(n=>{console.debug("Word Companion heartbeat unavailable.",n)})},ny=async()=>{if(nf&&window.signalR&&!nt){nt=(new signalR.HubConnectionBuilder).withUrl("/hubs/word-companion-follow").withAutomaticReconnect().build();nt.onreconnected(pf);nt.on("ScanCurrentDocument",n=>{void wd(n)});nt.onclose(()=>{eu&&(window.clearInterval(eu),eu=null)});await nt.start();await nt.invoke("RegisterCompanion",gv());eu=window.setInterval(pf,25e3)}};window.addEventListener("beforeunload",()=>{eu&&(window.clearInterval(eu),eu=null),nt&&nt.stop().catch(()=>{})});const ui=t=>n(gp,t),ft=()=>{const n=gr(),t=au(),i=tf&&Date.now()-se>=750;sr&&(sr.disabled=!n||!t||he);fo&&(fo.disabled=!n||!String(df?.value||"").trim()||he);ih&&(ih.disabled=!wr?.canImport||!oe||he);eo&&(eo.disabled=!i||yh||ty().length===0)},vu=n=>{r(kp,!n);r(yp,n);pc();gc();for(const t of yl)r(t,n?!0:t.dataset.tabPanel!=="scene"&&!t.classList.contains("is-active"));n||lv()},nl=()=>{if(et){if(ir.length===0){c(et,"No projects available.");return}ls(et,"Choose a project",ir,"projectId","title");const n=Number.parseInt(p?.value||"",10);Number.isInteger(n)&&n>0&&(et.value=String(n))}},rd=(n=null)=>{if(d){if(ii.length===0){c(d,"No books available.");return}ls(d,"Choose a book",ii.map(n=>({...n,displayTitle:dr(n)})),"bookId","displayTitle");const i=n||Number.parseInt(ot?.value||"",10),t=ii.find(n=>n.bookId===i);t&&(d.value=String(t.bookId));ft()}},ct=(t="Select a Project and Book to begin.")=>{wr=null,oe=null,hi=[],bo=!1,tf=!1,se=0,r(hr,!0),r(th,!0),r(pu,!0),r(bu,!0),hr&&(hr.innerHTML=""),ki&&(ki.innerHTML=""),n(wu,""),ui(t),ft()},ty=()=>ki?[...ki.querySelectorAll("input[type='checkbox']:checked")].map(n=>String(n.value||"").trim()).filter(Boolean):[],ud=t=>{if(hi=Array.isArray(t)?t:[],pu&&ki){if(ki.innerHTML="",r(pu,!1),r(bu,!bo||hi.length===0),r(eo,!1),r(pl,!1),r(wl,!0),hi.length===0){n(wu,"No likely major characters found.");ft();return}n(wu,`${hi.length} likely major characters found.`);for(const n of hi){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",ft);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);ki.append(i)}ft()}},fd=n=>{if(hr){hr.innerHTML="";const t=document.createElement("strong");t.textContent="Manuscript Analysis";hr.append(t);const i=document.createElement("dl"),u=[["Project",n.projectTitle],["Book",dr({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)}hr.append(i);r(hr,!1);r(th,!1);ui(n.message||"Preview generated.");ft()}},tl=()=>{const n=window.Office?.context?.document?.settings;if(!n)return null;const u=String(n.get(bi.documentGuid)||"").trim(),t=Number.parseInt(n.get(bi.bookId)||"",10),i=Number.parseInt(n.get(bi.projectId)||"",10),r=Number.parseInt(n.get(bi.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}},ed=()=>{const i=ri||tl();if(i)return i;const r=ur?.manuscriptDocument?.documentGuid||"",n=Number.parseInt(ur?.bookId||"",10),t=Number.parseInt(ur?.projectId||"",10);return r&&Number.isInteger(n)&&n>0&&Number.isInteger(t)&&t>0?{documentGuid:r,bookId:n,projectId:t,bindingVersion:Number.parseInt(ur?.manuscriptDocument?.bindingVersion||"",10)||1}:null},od=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(bi.documentGuid,n.documentGuid);r.set(bi.bookId,String(n.bookId));r.set(bi.projectId,String(n.projectId));r.set(bi.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."))})}),iy=()=>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(bi))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."))})}),ry=()=>ri?.documentGuid?ri.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)),or=()=>ri?.documentGuid||tl()?.documentGuid||"",pi=()=>{const t=ni(),n=h();ra&&(ra.textContent=t?.title||"(Not connected)");ua&&(ua.textContent=n?dr(n):"(Not connected)");aa&&(aa.textContent=n?"":"Select a PlotDirector book before refreshing.");is();gc()},uy=()=>{const t=Array.isArray(ht.characters)?ht.characters.length:0,i=Array.isArray(ht.assets)?ht.assets.length:0,r=Array.isArray(ht.locations)?ht.locations.length:0;n(hw,`Characters (${t})`);n(cw,`Assets (${i})`);n(lw,`Locations (${r})`)},ge=n=>{const t=n?.styleBuiltIn||n?.style||"";return String(t).replace(/\s+/g,"").toLowerCase()},wi=(n,t)=>{const i=ge(n);return i===`heading${t}`||i.endsWith(`.heading${t}`)||i.includes(`heading${t}`)},wf=n=>{const t=String(n||"").trim().split(/\s+/).filter(Boolean);return t.length},sd=(n,t)=>String(n?.text||"").trim()===String(t?.text||"").trim()&&ge(n)===ge(t),fy=(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},ey=n=>{try{return Array.isArray(n?.contentControls?.items)?n.contentControls.items:[]}catch(t){return console.warn("Word paragraph content controls were not available.",t),[]}},as=(n,t)=>{const i=ey(n);for(const n of i){const i=fy(n.tag,t);if(i)return i}return null},hd=(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(wi(n,1)){o+=1;i={index:u.length+1,paragraphIndex:t,title:f,anchorId:as(n,"PD-CHAPTER"),scenes:[]};u.push(i);r=null;return}if(wi(n,2)){if(s+=1,!i)return;r={index:i.scenes.length+1,paragraphIndex:t,title:f,anchorId:as(n,"PD-SCENE"),wordCount:0};i.scenes.push(r);return}r&&(r.wordCount+=wf(f))}});const h=t.find(n=>String(n.text||"").trim()),f=h?n.findIndex(n=>sd(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}},cd=(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),wi(n,1)){i={title:h,sortOrder:(r.length+1)*10,wordCount:0,scenes:[]};r.push(i);u=o();f+=wf(h);return}if(i&&u){if(t&&s.has(h)){u=o();return}const c=wf(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")}},vs=new Set(["***","###"]),ld=(n,t)=>{const i=String(n?.text||"").trim();return{index:t,text:i,styleBuiltIn:n?.styleBuiltIn||"",style:n?.style||"",wordCount:wf(i),chapterAnchorId:null,sceneAnchorId:null}},oy=(n,t)=>{n&&(n.endParagraphIndex=t)},sy=(n,t)=>{n&&(n.endParagraphIndex=t,oy(n.scenes.at(-1),t))},ad=(n,t)=>{const u=n.map(ld),f=[];let o=0,s=0,i=null,r=null,e=0;const h=(n,t=null,u=null)=>i?(oy(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(wi(n,1)){sy(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&&vs.has(n.text)){s+=1;h(n);continue}i.wordCount+=n.wordCount;r&&(r.wordCount+=n.wordCount);e+=n.wordCount}}return sy(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}},vd=new Set(["Chapter","Scene","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday","Sunday","January","February","March","April","May","June","July","August","September","October","November","December","The","A","An","And","But","Or","If","Then","When","Where","This","That","These","Those","He","She","They","We","I","You","It","His","Her","Their","Our","Your","My","Word","PlotDirector"]),yd=n=>{const t=new Map,i=String(n||"").match(/\b[A-Z][a-z]+(?:\s+[A-Z][a-z]+){0,2}\b/g)||[];for(const n of i){const i=n.trim(),r=i.split(/\s+/);r.some(n=>vd.has(n))||i.length<3||t.set(i,(t.get(i)||0)+1)}return[...t.entries()].filter(([,n])=>n>=2).sort((n,t)=>t[1]-n[1]||n[0].localeCompare(t[0])).slice(0,40).map(([t,n],i)=>({temporaryCharacterKey:`character-${i+1}`,name:t,mentionCount:n,suggestedImportance:null}))},pd=(n,t)=>{const u=[],f=[],s=[];let i=null,r=null,e=0,h=0;const c=()=>{r=null},l=(n,t,r=false)=>(c(),i={temporaryChapterKey:`chapter-${u.length+1}`,chapterNumber:u.length+1,title:t,wordCount:0,startPosition:n?.index??null,existingChapterID:r?null:as(n,"PD-CHAPTER")},u.push(i),o(n,null),i),o=(n,t=null)=>i?(c(),r={temporarySceneKey:`scene-${f.length+1}`,temporaryChapterKey:i.temporaryChapterKey,sceneNumberWithinChapter:f.filter(n=>n.temporaryChapterKey===i.temporaryChapterKey).length+1,title:t,wordCount:0,openingTextPreview:"",startPosition:n?.index??null,existingSceneID:n?as(n,"PD-SCENE"):null},f.push(r),r):null,a=(n,t)=>{!i||!r||t<=0||(i.wordCount+=t,r.wordCount+=t,r.openingTextPreview||!n||vs.has(n)||(r.openingTextPreview=n.length>180?`${n.slice(0,177)}...`:n))};if(n.forEach((n,t)=>{n.index=t;const r=String(n.text||"").trim();if(r){s.push(r);const u=wf(r);if(wi(n,1)){h+=1;l(n,r);e+=u;i.wordCount+=u;return}if(i||l(n,"Opening pages",!0),wi(n,2)){o(n,r);e+=u;a(r,u);return}if(vs.has(r)){o(n,null);return}e+=u;a(r,u)}}),e===0)throw new Error("The document appears to be empty. Add manuscript text, then try again.");if(h===0)throw new Error("No chapters were detected. Make sure your chapter headings use Heading 1, or add clear chapter headings.");const v=yd(s.join("\n"));return{previewID:"00000000-0000-0000-0000-000000000000",userID:t.userID||t.UserID||0,onboardingID:t.onboardingID||t.OnboardingID||0,projectID:t.projectID||t.ProjectID||0,bookID:t.bookID||t.BookID||0,source:"WordCompanion",documentTitle:dv()||"Word manuscript",companionDocumentIdentifier:or(),totalWordCount:e,chapterCount:u.length,sceneCount:f.length,characterCandidateCount:v.length,createdUtc:(new Date).toISOString(),chapters:u,scenes:f,characterCandidates:v}},ys=(n,t,i)=>n?.[t]??n?.[i]??null,bf=async(n,t,i,r={})=>{nt&&nt.state===signalR.HubConnectionState.Connected&&await nt.invoke("ReportOnboardingScanProgress",{userID:ys(n,"userID","UserID"),onboardingID:ys(n,"onboardingID","OnboardingID"),message:t,percentComplete:i,chapterCount:r.chapterCount||0,sceneCount:r.sceneCount||0,characterCandidateCount:r.characterCandidateCount||0,totalWordCount:r.totalWordCount||0})},hy=async(n,t)=>{nt&&nt.state===signalR.HubConnectionState.Connected&&await nt.invoke("FailOnboardingScan",{userID:ys(n,"userID","UserID"),onboardingID:ys(n,"onboardingID","OnboardingID"),message:t})},wd=async n=>{if(!it||!window.Word){await hy(n,"Open your manuscript in Microsoft Word, then try the scan again.");return}try{await bf(n,"Preparing document scan",5);const i=await window.Word.run(async t=>{const i=t.document.body.paragraphs;i.load("items/text,items/style,items/styleBuiltIn");await t.sync();await bf(n,"Detecting chapters",20);for(const n of i.items)(wi(n,1)||wi(n,2))&&n.contentControls.load("items/tag,title");return await t.sync(),i.items}),t=pd(i,n);await bf(n,"Detecting scene breaks",50,t);await bf(n,`Scene ${t.sceneCount} of ${t.sceneCount} scanned`,70,t);await bf(n,"Finding character candidates",85,t);await bf(n,"Preparing preview",95,t);await nt.invoke("CompleteOnboardingScan",t)}catch(t){console.error("Unable to complete onboarding scan.",t);await hy(n,bd(t))}},bd=n=>{const t=e(n);return/No chapters were detected/i.test(t)?"No chapters were detected. Make sure your chapter headings use Heading 1, or add clear chapter headings.":/empty/i.test(t)?"The document appears to be empty. Add manuscript text, then try again.":/network|disconnect|connection/i.test(t)?"The Word Companion disconnected before the scan finished. Reopen Word and try again.":`The scan could not finish: ${t}`},kd=(n,t)=>String(n?.text||"").trim()===String(t?.text||"").trim()&&ge(n)===ge(t),cy=(n,t)=>{const r=(t||[]).find(n=>String(n.text||"").trim());if(!n||!r)return n?.currentParagraphIndex??-1;const i=n.paragraphs.filter(n=>kd(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,gd=(n,t)=>!n||!Number.isInteger(t)||t<0?null:n.scenes.filter(n=>n.startParagraphIndex<=t).at(-1)||null,ly=n=>{if(!l)return!1;const t=dd(l,n),i=gd(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:(lf(t?.title,i?.title,i?.wordCount,t?.anchorId,i?.anchorId,t?.index),r)},il=n=>{if(gf){if(gf.innerHTML="",n.chapters.length===0){const n=document.createElement("p");n.textContent="No chapters detected. Use Heading 1 for chapter titles.";gf.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.";gf.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)}gf.append(n)}}},ay=[{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"}],ng={alreadyLinked:"Already Linked",couldLink:"Could Link",wouldCreateChapter:"Would Create Chapter",wouldCreateScene:"Would Create Scene",manualReview:"Manual Review Required"},tg={alreadyLinked:"✓",couldLink:"~",wouldCreateChapter:"+",wouldCreateScene:"+",manualReview:"?"},ps=(n,t)=>ut(n,t).toLocaleLowerCase(),fi=({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:""}),ei=(n,t)=>{Array.isArray(n[t.category])&&n[t.category].push(t)},rl=n=>Array.isArray(n?.scenes)?n.scenes:[],ig=(n,t)=>{for(const i of n){const n=rl(i).find(n=>n.sceneId===t);if(n)return{chapter:i,scene:n}}return null},rg=(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=fi({category:"alreadyLinked",action:"alreadyLinked",type:"Chapter",wordTitle:n.title,match:`Chapter ${r.chapterId}: ${r.title}`,anchorStatus:u,pdChapter:r,wordChapter:n});return ei(i,t),t}const f=fi({category:"manualReview",action:"manualReview",type:"Chapter",wordTitle:n.title,anchorStatus:`${u} not found`,matches:[],pdChapter:null,wordChapter:n});return ei(i,f),f}const r=t.filter(t=>ps(t.title,"chapter")===ps(n.title,"chapter"));if(r.length===1){const t=fi({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 ei(i,t),t}if(r.length>1){const t=fi({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 ei(i,t),t}const f=fi({category:"wouldCreateChapters",action:"wouldCreateChapter",type:"Chapter",wordTitle:n.title,anchorStatus:u,pdChapter:null,wordChapter:n});return ei(i,f),f},ug=(n,t,i,r)=>{const u=n.anchorId?`PD-SCENE-${n.anchorId}`:"None";if(n.anchorId){const f=ig(i,n.anchorId);if(f){ei(r,fi({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}ei(r,fi({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"){ei(r,fi({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){ei(r,fi({category:"wouldCreateScenes",action:"wouldCreateScene",type:"Scene",wordTitle:n.title,anchorStatus:u,wordChapter:t?.wordChapter,wordScene:n,parentItem:t}));return}const f=rl(t.pdChapter).filter(t=>ps(t.title,"scene")===ps(n.title,"scene"));if(f.length===1){ei(r,fi({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){ei(r,fi({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}ei(r,fi({category:"wouldCreateScenes",action:"wouldCreateScene",type:"Scene",wordTitle:n.title,anchorStatus:u,pdChapter:t.pdChapter,wordChapter:t.wordChapter,wordScene:n,parentItem:t}))},fg=(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=rg(t,r,i);for(const u of rl(t))ug(u,n,r,i)}return i},eg=n=>{const t=document.createElement("article");t.className="word-companion-preview-item";const i=document.createElement("strong");i.textContent=`${tg[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: ${ng[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},vy=n=>{if(gu){gu.innerHTML="";for(const t of ay){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(eg(n));gu.append(i)}ts()}},kf=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)(wi(n,1)||wi(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=hd(t.items,i.items);return r.bodyParagraphs=t.items,r},yy=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=ad(u.items,t),e=cy(r,f.items);return l=r,ly(e),i("Cache rebuilt."),r},btt=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(),ti=n=>!!n&&!n.stale&&n.generation===le&&n.documentGuid===or()&&n.sceneId===u,py=async(n={})=>{const r=!!n.includeSelection,w=n.updateDisplay!==!1,f=le,b=y(),k=ni(),d=h(),e=l?.currentScene;if(!e||!Number.isInteger(e.startParagraphIndex)||!Number.isInteger(e.endParagraphIndex)||!kt||!it||!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!==le)return ci(`Office snapshot: ${c}ms stale`),{stale:!0,generation:f};let a=!1;if(r){const n=cy(l,s.selectionParagraphs);a=ly(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&&!vs.has(t)&&n!==tt&&(v.push(t),i+=wf(t))}t.wordCount=i;w&&lf(l.currentChapter?.title,t.title,i,l.currentChapter?.anchorId,t.anchorId,l.currentChapter?.index);const p={documentGuid:or(),projectId:k?.projectId||null,bookId:d?.bookId||null,chapterId:o,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 ae=p,ci(`Office snapshot: ${c}ms; scene words: ${i}; total snapshot: ${Math.round(y()-b)}ms`),p},wy=async()=>ti(ae)?ae:await py(),by=async()=>{if(t(""),!kt||!it||!window.Word||typeof window.Word.run!="function")return lf(null,null,null),t("Word document access is unavailable."),f({lastScan:"Failure",lastError:"Word document access is unavailable."}),!1;nu&&(nu.disabled=!0,nu.textContent="Scanning...");t("Scanning document structure...");try{const n=await window.Word.run(async n=>{const t=h();return t?await yy(n,!!t.usesExplicitScenes):(l=null,await kf(n))});return l||lf(n.currentChapter?.title,n.currentScene?.title,n.currentScene?.wordCount,n.currentChapter?.anchorId,n.currentScene?.anchorId,n.currentChapter?.index),tt("Not detected"),il(n),t(""),f({lastScan:"Success",lastError:"-",paragraphs:String(n.paragraphCount),heading1:String(n.heading1Count),heading2:String(n.heading2Count)}),!0}catch(n){const i=e(n);return console.error("Unable to scan the Word document.",n),lf(null,null,null),t(`Unable to scan the Word document: ${i}`),f({lastScan:"Failure",lastError:i}),!1}finally{nu&&(nu.disabled=!1,nu.textContent="Refresh Document Structure")}},ky=async()=>{const n=h();if(!ni()||!n)return er("Select a project and book to analyse manuscript structure."),!1;if(!kt||!it||!window.Word||typeof window.Word.run!="function")return er("Word document access is unavailable."),f({lastScan:"Failure",lastError:"Word document access is unavailable."}),!1;cr&&(cr.disabled=!0,cr.textContent="Analysing...");er("Analysing manuscript structure...");try{const t=await window.Word.run(async n=>await kf(n)),i=await at(`/api/word-companion/books/${n.bookId}/structure`);return lf(t.currentChapter?.title,t.currentScene?.title,t.currentScene?.wordCount,t.currentChapter?.anchorId,t.currentScene?.anchorId,t.currentChapter?.index),il(t),vi=fg(t,i),vy(vi),er("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),er("Unable to analyse manuscript structure."),f({lastScan:"Failure",lastError:e(t)}),!1}finally{cr&&(cr.textContent="Analyse Manuscript Structure",is())}},og=(n=vi)=>{const t=ou(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}},sg=n=>{if(ch){ch.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)}ch.append(t)}},ul=n=>{ko&&(ko(n),ko=null),tr?.close?tr.close():tr&&(tr.hidden=!0)},hg=()=>{const n=og();return(sg(n),!tr)?Promise.resolve(window.confirm(`Apply Structure Sync? This will: @@ -6,7 +6,7 @@ This will: - 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: +Manual review items will be skipped.`)):new Promise(n=>{ko=n,tr.showModal?tr.showModal():tr.hidden=!1})},no=(n,t,i="")=>{n.result=t,n.error=i},dy=n=>Number.isInteger(n?.wordChapter?.index)&&n.wordChapter.index>0?n.wordChapter.index*10:null,gy=n=>Number.isInteger(n?.wordScene?.index)&&n.wordScene.index>0?n.wordScene.index*10:null,cg=async(n,t)=>{const i=await lt(`/api/word-companion/books/${n}/chapters`,{title:lu(t.wordTitle),sortOrder:dy(t)});if(!i?.chapterId)throw new Error("Chapter creation did not return a chapter ID.");su();t.pdChapter={chapterId:i.chapterId,title:i.title||t.wordTitle,sortOrder:i.sortOrder||dy(t)||0,scenes:[]};t.match=`Chapter ${t.pdChapter.chapterId}: ${t.pdChapter.title}`},lg=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 lt(`/api/word-companion/books/${n}/chapters/${i.chapterId}/scenes`,{sceneTitle:lu(t.wordTitle),sortOrder:gy(t)});if(!r?.sceneId)throw new Error("Scene creation did not return a scene ID.");su();t.pdChapter=i;t.pdScene={sceneId:r.sceneId,title:t.wordTitle,sortOrder:gy(t)||0};t.match=`Scene ${t.pdScene.sceneId}: ${t.pdScene.title}`},ag=async n=>{if(!kt||!it||!window.Word||typeof window.Word.run!="function")throw new Error("Word document access is unavailable.");await window.Word.run(async t=>{const u=await kf(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.");to(i,"PlotDirector Chapter",`PD-CHAPTER-${t}`,"PD-CHAPTER")}else{const t=n.pdScene?.sceneId;if(!t)throw new Error("Scene ID is not available.");to(i,"PlotDirector Scene",`PD-SCENE-${t}`,"PD-SCENE")}await t.sync()})},vg=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.`,yg=async()=>{const r=h();if(!r||ou().length===0||ce){ts();return}const f=await hg();if(f){ce=!0;is();er("Applying structure sync...");const n={createdChapters:0,createdScenes:0,linkedChapters:0,linkedScenes:0,skipped:Array.isArray(vi?.manualReview)?vi.manualReview.length:0,failed:0},o=ou().filter(n=>n.category==="couldLink"&&n.type==="Chapter"),s=ou().filter(n=>n.category==="wouldCreateChapters"),c=ou().filter(n=>n.category==="couldLink"&&n.type==="Scene"),l=ou().filter(n=>n.category==="wouldCreateScenes"),u=[];let t="";const i=(n,t)=>{u.push({item:n,counterName:t})},a=async(t,i)=>{try{await ag(t);no(t,"Success");n[i]+=1}catch(r){no(t,"Failed",e(r));n.failed+=1}};try{for(const n of o)i(n,"linkedChapters");for(const t of s)try{await cg(r.bookId,t);i(t,"createdChapters")}catch(u){no(t,"Failed",e(u));n.failed+=1}for(const n of c)i(n,"linkedScenes");for(const t of l)try{await lg(r.bookId,t);i(t,"createdScenes")}catch(u){no(t,"Failed",e(u));n.failed+=1}for(const n of u)await a(n.item,n.counterName);for(const n of vi.manualReview||[])no(n,"Skipped");vy(vi);t=vg(n);er(t)}finally{ce=!1;is();t&&(su(),await ky(),er(`${t} Preview refreshed.`))}}},pg=async()=>{iu&&(iu.disabled=!0,iu.textContent="Running...");try{if(await kv(),!kt||!it||!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=e(n);f({lastScan:"Failure",lastError:i});t(`Unable to scan the Word document: ${i}`)}finally{iu&&(iu.disabled=!1,iu.textContent="Run Diagnostics")}},at=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()},lt=(n,t)=>at(n,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)}),wg=async()=>{const n=gr(),t=au();if(!n||!t){ct("Select a Project and Book to begin.");return}if(!kt||!it||!window.Word||typeof window.Word.run!="function"){ct("Word document access is unavailable.");return}sr&&(sr.disabled=!0,sr.textContent="Scanning...");ui("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(),cd(i.items,!!t.usesExplicitScenes)});oe=i;bo=!1;const u=await lt("/api/word-companion/manuscript/analyse",{projectId:n.projectId,bookId:t.bookId,documentGuid:ry(),chapterCount:i.chapterCount,sceneCount:i.sceneCount,wordCount:i.wordCount});wr=u;fd(u);try{const t=await lt("/api/word-companion/manuscript/discover-characters",{projectId:n.projectId,documentText:i.documentText||""});hi=Array.isArray(t?.candidates)?t.candidates:[];r(pu,!0);r(bu,!0)}catch(e){console.error("Unable to discover character candidates.",e);hi=[];r(pu,!0);r(bu,!0)}f({lastScan:"Success",lastError:"-",paragraphs:"-",heading1:String(i.chapterCount),heading2:"-"})}catch(i){console.error("Unable to scan manuscript for first-run import.",i);ct(`Unable to scan manuscript: ${e(i)}`);f({lastScan:"Failure",lastError:e(i)})}finally{sr&&(sr.textContent="Scan Manuscript");ft()}},bg=()=>new Promise(n=>{if(!ku||typeof ku.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=>{bl?.removeEventListener("click",i),kl?.removeEventListener("click",r),ku?.removeEventListener("cancel",u),ku?.close(),n(t)},i=()=>t(!0),r=()=>t(!1),u=n=>{n.preventDefault(),t(!1)};bl?.addEventListener("click",i);kl?.addEventListener("click",r);ku?.addEventListener("cancel",u);ku.showModal()}),kg=()=>new Promise(n=>{if(!du||typeof du.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=>{dl?.removeEventListener("click",i),gl?.removeEventListener("click",r),du?.removeEventListener("cancel",u),du?.close(),n(t)},i=()=>t(!0),r=()=>t(!1),u=n=>{n.preventDefault(),t(!1)};dl?.addEventListener("click",i);gl?.addEventListener("click",r);du?.addEventListener("cancel",u);du.showModal()}),dg=async()=>{const i=gr(),u=au();if(!i||!u||!oe||!wr?.canImport){ft();return}let n=!1;if(wr.requiresReplaceConfirmation&&(n=await bg(),!n)){ui("Import cancelled.");return}he=!0;ft();ui("Importing manuscript structure...");try{const h=ry(),f=await lt("/api/word-companion/manuscript/import",{projectId:i.projectId,bookId:u.bookId,documentGuid:h,bindingVersion:1,replacePlanned:n,chapters:oe.chapters});if(!f.imported){wr={...wr,canImport:!f.requiresReplaceConfirmation,requiresReplaceConfirmation:!!f.requiresReplaceConfirmation,message:f.message||wr.message};ui(f.message||"Import was not completed.");ft();return}const e=f.manuscriptDocument;await od({documentGuid:e?.documentGuid||h,bookId:f.bookId,projectId:f.projectId,bindingVersion:e?.bindingVersion||1});ri={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=hi;await yu(f.projectId,f.bookId);be("Connected");bo=!0;tf=o.length>0;se=tf?Date.now():0;hi=o;o.length>0?(vu(!0),ud(o),r(th,!0),r(bu,!1),ui(f.message||"Manuscript structure imported."),t(f.message||"Manuscript structure imported."),window.setTimeout(ft,750)):await ws(f.message||"Manuscript structure imported.")}catch(f){console.error("Unable to import manuscript structure.",f);ui(`Unable to import manuscript: ${e(f)}`)}finally{he=!1;ft()}},ws=async(n="")=>{tf=!1;se=0;hi=[];ki&&(ki.innerHTML="");r(pu,!0);vu(!1);ui("");r(bu,!0);n&&t(n);const i=await np();n&&i&&w&&t(n)},gg=async()=>{const i=gr()||ni(),r=ty();if(!tf||Date.now()-se<750||!i||r.length===0){ft();return}yh=!0;ft();n(wu,"Creating selected characters...");try{const u=await lt("/api/word-companion/manuscript/create-discovered-characters",{projectId:i.projectId,candidateNames:r});t(u.message||"Characters created.");n(wu,`${u.createdCount||0} created, ${u.skippedCount||0} already existed.`);await ws(u.message||"Characters created.")}catch(u){console.error("Unable to create discovered characters.",u);n(wu,`Unable to create characters: ${e(u)}`)}finally{yh=!1;ft()}},nn=async(n=u,t=o)=>{const f=ni(),e=h(),c=or(),r=Number.parseInt(n||"",10),s=Number.parseInt(t||"",10);if(f&&e&&c&&Number.isInteger(r)&&!(r<=0)&&Number.isInteger(s)&&!(s<=0)){const l=`${f.projectId}:${e.bookId}:${r}`;if(cc!==l){cc=l;try{await lt("/api/word-companion/runtime/current-scene",{documentGuid:c,projectId:f.projectId,bookId:e.bookId,chapterId:s,sceneId:r});i("Current scene follow event sent.")}catch(a){cc="";console.debug("Unable to notify PlotDirector of current scene.",a)}}}},np=async()=>{const n=h();if(!n||!kt||!it||!window.Word||typeof window.Word.run!="function")return!1;t("Reading manuscript position...");try{su();await Promise.all([ol(!0),sl(!0),hl(!0)]);await ss(n.bookId);const i=await window.Word.run(async t=>await yy(t,!!n.usesExplicitScenes));return(il(i),f({lastScan:"Success",lastError:"-",paragraphs:String(i.paragraphCount),heading1:String(i.heading1Count),heading2:String(i.heading2Count)}),!w)?(tt("Not detected"),li(!1),k("Live"),t("Manuscript linked. Click inside a chapter to show the current scene."),!0):(rt?await uo():await ll(),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: ${e(i)}`),f({lastScan:"Failure",lastError:e(i)}),!1}},tn=async()=>{if(!nf||!it){vu(!1);return}const n=tl();if(n)try{const t=await at(`/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()){ri=n;vv(t);v(s.projectId,t.projectId);v(s.bookId,t.bookId);p&&(p.value=String(t.projectId));await yu(t.projectId,t.bookId);vu(!1);be("Connected");hf("Bound manuscript detected.");await np();return}}catch{}ri=null;yf();nl();gr()?await yu(gr().projectId,null):c(d,"Select a project first");ct("Select a Project and Book to begin.");vu(!0)},tp=async n=>{ri=null,ap(),af(),yf(),tt("Not detected"),k("Manual"),be("Connected"),v(s.projectId,null),v(s.bookId,null),p&&(p.value=""),c(ot,"Select a project first"),nl(),et&&(et.value=""),c(d,"Select a project first"),ct(n||"Select a Project and Book to begin."),t(n||"Manuscript unlinked."),gt("No resolved PlotDirector scene."),vu(!0),pc()},rn=async()=>{console.debug("Word Companion unlink clicked");t("Preparing to unlink manuscript...");const n=ed();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 kg();if(!r){t("Unlink cancelled.");return}t("Unlinking manuscript...");lr&&(lr.disabled=!0,lr.textContent="Unlinking...");try{await lt("/api/word-companion/manuscript/unlink",{documentGuid:n.documentGuid,bookId:n.bookId});await iy();await tp("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 iy();await tp("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: ${e(n)}`)}else t(`Unable to unlink manuscript: ${e(i)}`)}finally{lr&&(lr.disabled=!1,lr.textContent="Unlink this Word document");pc()}},ktt=(n,t)=>at(n,{method:"PATCH",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)}),un=(n,t)=>at(n,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)}),bs=n=>n?[...n.querySelectorAll("input[type='checkbox']:checked")].map(n=>Number.parseInt(n.value||"",10)).filter(n=>Number.isInteger(n)&&n>0):[],ip=()=>{const n=Number.parseInt(vr?.value||"",10);return Number.isInteger(n)&&n>0?n:null},fn=()=>{const n=new Set(bs(ne)),t=new Set(bs(te));ht={characters:ht.characters.map(t=>({...t,linked:n.has(Number.parseInt(es(t,"character"),10))})),assets:ht.assets.map(n=>({...n,linked:t.has(Number.parseInt(es(n,"asset"),10))})),locations:ht.locations,primaryLocationId:ip()}},en=async()=>{if(!u)return!1;if(!kt||!it||!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 kf(n)),r=n.currentChapter?.title||"",f=n.currentScene?.title||"",t=n.currentScene?.anchorId||null,i=n.currentChapter?.anchorId||null,e=Number.isInteger(t)&&t===u,s=Number.isInteger(i)&&i===o,h=ut(f,"scene").toLocaleLowerCase()===ut(ns||rt,"scene").toLocaleLowerCase(),c=ut(r,"chapter").toLocaleLowerCase()===ut(lc||w,"chapter").toLocaleLowerCase(),l=e||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:e(n)}),li(!0,"The Word cursor is now in a different scene. Refresh Current Scene before saving."),!1}},on=async()=>{if(!u){hu("Link a PlotDirector scene first.");return}if(st||!await en()){hu("The Word cursor is now in a different scene. Refresh Current Scene before saving.");return}di&&(di.disabled=!0,di.textContent="Saving...");try{await un(`/api/word-companion/scenes/${u}/links`,{characterIds:bs(ne),assetIds:bs(te),locationIds:[],primaryLocationId:ip()});fn();hu("Scene links saved successfully.");f({lastError:"-"})}catch(n){hu("Unable to save scene links.");f({lastError:e(n)})}finally{di&&(di.disabled=!u||st,di.textContent="Save Scene Links")}},sn=(n,t)=>{const i=ey(n);return i.find(n=>fy(n.tag,t))||null},to=(n,t,i,r)=>{const f=sn(n,r),u=f||n.getRange().insertContentControl();return u.title=t,u.tag=i,u.appearance="Hidden",u},rp=async(n="PlotDirector IDs attached successfully.")=>{if(!u||!o)return t("No resolved PlotDirector scene."),!1;const r=g&&g!==u||b&&b!==o;if(r&&!window.confirm("This scene is already linked. Replace the existing PlotDirector link?"))return!1;if(!kt||!it||!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 kf(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.");to(i,"PlotDirector Chapter",`PD-CHAPTER-${o}`,"PD-CHAPTER");to(r,"PlotDirector Scene",`PD-SCENE-${u}`,"PD-SCENE");await n.sync()}),b=o,g=u,dt=!1,rr="SceneID Anchor",re="Anchor",t(n),f({lastError:"-"}),fr(),!0}catch(i){return console.error("Unable to attach PlotDirector IDs.",i),t("Unable to attach PlotDirector IDs."),f({lastError:e(i)}),fr(),!1}finally{ai&&(ai.textContent=g===u?"Attach PlotDirector IDs":"Reattach PlotDirector IDs",fr())}},up=async n=>{if(!o)return yi("No resolved PlotDirector chapter."),!1;if(!kt||!it||!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 kf(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.");to(i,"PlotDirector Chapter",`PD-CHAPTER-${o}`,"PD-CHAPTER");await n.sync()}),b=o,dt=!1,rr="ChapterID Anchor",re="Chapter anchor",yi(n),t(n),f({lastError:"-"}),fr(),!0}catch(i){return console.error("Unable to attach PlotDirector chapter ID.",i),yi("Unable to attach PlotDirector chapter ID."),f({lastError:e(i)}),fr(),!1}},hn=async()=>{await rp()},fp=async n=>{ke(),await uo(),t(n),yi(n)},fl=n=>{po&&(po(n),po=null),nr?.close?nr.close():nr&&(nr.hidden=!0)},cn=(t,i)=>(n(rb,`"${t}"`),n(ub,`"${i}"`),!nr)?Promise.resolve(window.confirm(`Create PlotDirector chapter: "${t}" @@ -14,7 +14,7 @@ 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: +?`)):new Promise(n=>{po=n,nr.showModal?nr.showModal():nr.hidden=!1}),ln=async()=>{const n=h();if(!n||!w||o){us("Chapter not found in PlotDirector.");return}const t=lu(w),i=await cn(t,dr(n)||"selected book");if(i){yt&&(yt.disabled=!0,yt.textContent="Creating...");try{const r=Number.isInteger(ie)&&ie>0?ie*10:null,i=await lt(`/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.");su();kr(null,i.chapterId,"Manual chapter link");const u=await up("Chapter created and linked successfully.");if(!u)return;await fp("Chapter created and linked successfully.")}catch(r){yi("Unable to create chapter.");f({lastError:e(r)})}finally{yt&&(yt.disabled=!kc(),yt.textContent="Create Chapter in PlotDirector")}}},ep=()=>{if(lo){const n=lu(co?.value||"").toLocaleLowerCase(),t=vh.filter(t=>!n||String(t.title||"").toLocaleLowerCase().includes(n));if(lo.innerHTML="",fe=null,si&&(si.disabled=!0),t.length===0){const n=document.createElement("p");n.className="word-companion-empty-list";n.textContent="No chapters found.";lo.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",()=>{fe=i,si&&(si.disabled=!1)});const u=document.createElement("span");u.textContent=n.title||"Untitled chapter";r.append(t,u);lo.append(r)}}}},an=async()=>{const t=h();if(!t||!w||o){us("Chapter not found in PlotDirector.");return}pt&&(pt.disabled=!0,pt.textContent="Loading...");try{vh=await pv(t.bookId);fe=null;co&&(co.value="");n(hh,"");ep();pr?.showModal?pr.showModal():pr&&(pr.hidden=!1)}catch(i){yi("Unable to load chapters.");f({lastError:e(i)})}finally{pt&&(pt.disabled=!kc(),pt.textContent="Link to Existing Chapter")}},op=()=>{pr?.close?pr.close():pr&&(pr.hidden=!0)},vn=async()=>{const t=vh.find(n=>n.chapterId===fe);if(!t){n(hh,"Select a chapter.");return}si&&(si.disabled=!0,si.textContent="Linking...");try{kr(null,t.chapterId,"Manual chapter link");const n=await up("Chapter linked successfully.");if(!n)return;op();await fp("Chapter linked successfully.")}catch(i){n(hh,"Unable to link chapter.");yi("Unable to link chapter.");f({lastError:e(i)})}finally{si&&(si.disabled=!fe,si.textContent="Link Chapter")}},sp=async(n,t,i)=>{const r=h();if(!r)return cu("Select a PlotDirector book first."),!1;await hs(r.bookId,n,t,"Manual link","Title Match");const u=await rp(i);return u?(cu(i),fs(),!0):!1},el=n=>{yo&&(yo(n),yo=null),gi?.close?gi.close():gi&&(gi.hidden=!0)},yn=(t,i)=>(n(dw,`"${t}"`),n(gw,`"${i}"`),!gi)?Promise.resolve(window.confirm(`Create PlotDirector scene: "${t}" @@ -22,4 +22,4 @@ 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 +?`)):new Promise(n=>{yo=n,gi.showModal?gi.showModal():gi.hidden=!1}),pn=async()=>{const n=h();if(!n||!rt){de("Scene not found in PlotDirector.");return}const t=ut(rt,"scene"),i=ut(w,"chapter"),r=await yn(t,i);if(r){wt&&(wt.disabled=!0,wt.textContent="Creating...");try{const r=await bv();if(!r){de("This chapter does not exist in PlotDirector. Create or link the chapter first.");return}const i=await lt(`/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.");su();await sp(i.sceneId,i.chapterId,"Scene created and linked successfully.")}catch(u){cu("Unable to create scene.");f({lastError:e(u)})}finally{wt&&(wt.disabled=!dc(),wt.textContent="Create Scene in PlotDirector")}}},hp=()=>{if(ho){const n=lu(so?.value||"").toLocaleLowerCase(),t=ah.filter(t=>!n||String(t.title||"").toLocaleLowerCase().includes(n));if(ho.innerHTML="",ue=null,oi&&(oi.disabled=!0),t.length===0){const n=document.createElement("p");n.className="word-companion-empty-list";n.textContent="No scenes found.";ho.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",()=>{ue=i,oi&&(oi.disabled=!1)});const u=document.createElement("span");u.textContent=n.title||"Untitled scene";r.append(t,u);ho.append(r)}}}},wn=async()=>{const t=h();if(!t||!rt){de("Scene not found in PlotDirector.");return}bt&&(bt.disabled=!0,bt.textContent="Loading...");try{const r=await ss(t.bookId),i=wv(r);if(!i){de("This chapter does not exist in PlotDirector. Create or link the chapter first.");return}ah=Array.isArray(i.scenes)?i.scenes:[];ue=null;so&&(so.value="");n(sh,"");hp();yr?.showModal?yr.showModal():yr&&(yr.hidden=!1)}catch(i){cu("Unable to load scenes.");f({lastError:e(i)})}finally{bt&&(bt.disabled=!dc(),bt.textContent="Link to Existing Scene")}},cp=()=>{yr?.close?yr.close():yr&&(yr.hidden=!0)},bn=async()=>{const i=h(),t=ah.find(n=>n.sceneId===ue);if(!i||!t){n(sh,"Select a scene.");return}oi&&(oi.disabled=!0,oi.textContent="Linking...");try{const n=await bv();if(!n)throw new Error("This chapter does not exist in PlotDirector. Create or link the chapter first.");cp();await sp(t.sceneId,n.chapterId,"Scene linked successfully.")}catch(r){n(sh,"Unable to link scene.");cu("Unable to link scene.");f({lastError:e(r)})}finally{oi&&(oi.disabled=!ue,oi.textContent="Link Scene")}},io=()=>{uf=!1,bc(),pe&&(pe=!1,ks(1e3))},lp=async(t={})=>{const p=t.source||"manual",w=y();if(!u){gt("No resolved PlotDirector scene.");return}if(!o){gt("No resolved PlotDirector chapter.");return}if(st){gt("Refresh Current Scene before syncing.");return}const c=h(),l=or();if(!c||!l){gt("Bound manuscript metadata is unavailable.");return}if(uf){pe=!0;i("Scene sync deferred; sync already running.");return}af();uf=!0;pe=!1;let r=t.snapshot||null;try{r=ti(r)?r:await wy()}catch(v){gt("Sync failed");f({lastError:e(v)});i("Scene sync failed.");io();return}if(!ti(r)){gt("Waiting for typing to pause");i("Scene sync skipped (stale snapshot).");io();return}const s=r.wordCount;if(!Number.isInteger(s)||s<0){gt("Sync failed");i("Scene sync failed.");io();return}if(bh===r.sceneId&&kh===s){gt("Synced");i("Sync skipped (count unchanged).");io();return}i("Scene word count changed.");ar&&(ar.disabled=!0,ar.textContent="Syncing...");gt("Syncing...");i(`Scene sync started (${p}).`);try{const f=y(),e=await lt("/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`),!ti(r)){i("Scene sync result ignored (stale snapshot).");return}const t=e?.actualWords??s,u=new Date;bh=r.sceneId;kh=t;n(oh,a(t));n(uh,a(t));n(ga,rs(u));n(da,rs(u));gt("Synced");i("Scene sync succeeded.");ci(`Total word count sync: ${Math.round(y()-w)}ms`)}catch(v){gt("Sync failed");f({lastError:e(v)});i("Scene sync failed.")}finally{ar&&(ar.textContent="Sync Current Scene");io()}},ks=(n=4e3)=>{(af(),u&&o&&!st&&!uf)&&(ye=window.setTimeout(()=>{ye=null,void lp({source:"automatic"})},n))},ro=()=>{we&&(window.clearTimeout(we),we=null)},kn=()=>{dh=null,gh="",sf=[],nc=null,tc="",ro()},dn=()=>{ic=null,rc="",uu=[],uc=null,fc=""},gn=()=>{ec=null,oc="",fu=[],sc=null,hc=""},ol=async(n=false)=>{const t=ni(),r=or();if(!t||!r)return sf=[],[];if(!n&&dh===t.projectId&&gh===r&&sf.length>0)return sf;const u=await at(`/api/word-companion/runtime/characters?projectId=${encodeURIComponent(t.projectId)}&documentGuid=${encodeURIComponent(r)}`);return dh=t.projectId,gh=r,sf=Array.isArray(u?.characters)?u.characters:[],i("Character alias cache refreshed."),sf},sl=async(n=false)=>{const t=ni(),r=or();if(!t||!r)return uu=[],[];if(!n&&ic===t.projectId&&rc===r&&uu.length>0)return uu;const u=await at(`/api/word-companion/runtime/assets?projectId=${encodeURIComponent(t.projectId)}&documentGuid=${encodeURIComponent(r)}`);return ic=t.projectId,rc=r,uu=Array.isArray(u?.assets)?u.assets:[],i(`Asset cache loaded: ${uu.length} aliases.`),uu},hl=async(n=false)=>{const t=ni(),r=or();if(!t||!r)return fu=[],[];if(!n&&ec===t.projectId&&oc===r&&fu.length>0)return fu;const u=await at(`/api/word-companion/runtime/locations?projectId=${encodeURIComponent(t.projectId)}&documentGuid=${encodeURIComponent(r)}`);return ec=t.projectId,oc=r,fu=Array.isArray(u?.locations)?u.locations:[],i(`Location cache loaded: ${fu.length} aliases.`),fu},ntt=n=>String(n||"").replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),cl=(n,t)=>{const i=String(t||"").trim();if(!i)return!1;const r=ntt(i).replace(/\s+/g,"\\s+");return new RegExp(`(^|[^\\p{L}\\p{N}_])${r}(?=$|[^\\p{L}\\p{N}_])`,"iu").test(n)},ttt=(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)||cl(n,r.matchText)&&i.set(t,r.name||r.matchText||"Character")}return[...i.entries()].map(([n,t])=>({characterId:n,name:t}))},itt=(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)||cl(n,r.matchText)&&i.set(t,r.name||r.matchText||"Asset")}return[...i.entries()].map(([n,t])=>({assetId:n,name:t}))},rtt=n=>String(n||"").trim().split(/\s+/).filter(Boolean).length,utt=n=>{if(n?.excludeFromCompanionDetection)return!1;const t=Number.parseInt(n?.detectionPriority||"0",10);return rtt(n?.matchText)>=2||Number.isInteger(t)&&t>=75},ftt=(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)||!utt(r)||cl(n,r.matchText)&&i.set(t,r.name||r.matchText||"Location")}return[...i.entries()].map(([n,t])=>({locationId:n,name:t}))},ett=()=>{ff=!1,ef&&(ef=!1,ds(1500))},ott=async()=>{if(we=null,u&&!st&&or()){if(ff){ef=!0;i("Character suggestion detection deferred; detection already running.");return}ff=!0;ef=!1;const r=y();try{const n=await wy();if(!ti(n)){i("Runtime suggestions skipped (stale snapshot).");return}const[w,b,k]=await Promise.all([ol(),sl(),hl()]);if(!ti(n)){i("Runtime suggestions skipped after cache load (stale snapshot).");return}const s=n.sceneText||"",d=y(),o=ttt(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=itt(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=ftt(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&&(nc!==u||tc!==h)){if(!ti(n)){i("Character suggestions skipped before submit (stale snapshot).");return}const u=y(),r=await lt("/api/word-companion/runtime/character-suggestions",{documentGuid:n.documentGuid,sceneId:n.sceneId,characterIds:a});if(ci(`Character suggestion API: ${Math.round(y()-u)}ms`),!ti(n)){i("Character suggestion result ignored (stale snapshot).");return}if(nc=n.sceneId,tc=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&&(uc!==u||fc!==c)){if(!ti(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 lt("/api/word-companion/runtime/asset-suggestions",{documentGuid:n.documentGuid,sceneId:n.sceneId,assetIds:v});if(ci(`Asset suggestion API: ${Math.round(y()-u)}ms`),!ti(n)){i("Asset suggestion result ignored (stale snapshot).");return}if(uc=n.sceneId,fc=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&&(sc!==u||hc!==l)){if(!ti(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 lt("/api/word-companion/runtime/location-suggestions",{documentGuid:n.documentGuid,sceneId:n.sceneId,locationIds:p});if(ci(`Location suggestion API: ${Math.round(y()-u)}ms`),!ti(n)){i("Location suggestion result ignored (stale snapshot).");return}if(sc=n.sceneId,hc=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:e(n)})}finally{ett()}}},ds=(n=2500)=>{if(ro(),!u||st||ff){ff&&(ef=!0);return}we=window.setTimeout(()=>{void ott()},n)},ll=async()=>{const i=ni(),n=h(),r=ut(w,"chapter");if(vt({projectId:i?.projectId,projectTitle:i?.title,bookId:n?.bookId,bookTitle:n?dr(n):undefined,detectedChapter:w,detectedScene:rt,requestChapter:r,requestScene:"",result:"Not run",chapterId:null,sceneId:null}),!n)return tt("Not detected"),t("Select a PlotDirector book first."),!1;if(!w)return tt("Not detected"),t("No chapter detected in Word. Use Heading 1 for chapters."),!1;tt("Not detected");try{const f=await pv(n.bookId),i=b?f.find(n=>n.chapterId===b):null,e=r.toLocaleLowerCase(),o=e?f.find(n=>ut(n.title,"chapter").toLocaleLowerCase()===e):null,u=i||o;return u?(ke(),kr(null,u.chapterId,i?"Chapter anchor":"Chapter title"),rr=i?"ChapterID Anchor":"Title Match",cf("Chapter linked to PlotDirector"),t("No scene detected in Word. Use Heading 2 for scenes."),vt({result:"Chapter matched",chapterId:u.chapterId,sceneId:null}),!0):(kr(null),cf("Chapter not found in PlotDirector."),t("Chapter not found in PlotDirector."),vt({result:"Chapter not matched",chapterId:null,sceneId:null}),us("Chapter not found in PlotDirector."),!1)}catch(u){return tt("Not found in PlotDirector"),f({lastError:e(u)}),t("Unable to load PlotDirector chapter data."),!1}},uo=async()=>{const r=ni(),n=h(),u=ut(w,"chapter"),i=ut(rt,"scene");if(vt({projectId:r?.projectId,projectTitle:r?.title,bookId:n?.bookId,bookTitle:n?dr(n):undefined,detectedChapter:w,detectedScene:rt,requestChapter:u,requestScene:i,result:"Not run",chapterId:null,sceneId:null}),yc({anchorType:g?"SceneID Anchor":b?"ChapterID Anchor":"Title Match",storedSceneId:g,storedChapterId:b,resolutionMethod:"-"}),!n)return tt("Not detected"),t("Select a PlotDirector book first."),vt({result:"Error"}),!1;if(!w||!rt)return tt("Not detected"),t("No scene detected in Word. Use Heading 2 for scenes."),vt({result:"Error"}),!1;tu&&(tu.disabled=!0,tu.textContent="Refreshing...");const s=()=>{tu&&(tu.disabled=!1,tu.textContent="Refresh PlotDirector Scene")};tt("Not detected");t("Resolving PlotDirector scene...");try{if(g){const i=await kk(n.bookId,(n,t)=>t.sceneId===g);if(i)return vt({result:"Matched",chapterId:i.chapter.chapterId,sceneId:i.scene.sceneId}),await hs(n.bookId,i.scene.sceneId,i.chapter.chapterId,"Anchor","SceneID Anchor"),!0;dt=!0;rr="SceneID Anchor";cf("Not found in PlotDirector");t("Stored PlotDirector ID is invalid.");vt({result:"Error",chapterId:b,sceneId:g});yc({anchorType:"SceneID Anchor",storedSceneId:g,storedChapterId:b,resolutionMethod:"Anchor"})}if(b){const u=await ss(n.bookId),t=u.find(n=>n.chapterId===b),r=t?(Array.isArray(t.scenes)?t.scenes:[]).find(n=>ut(n.title,"scene").toLocaleLowerCase()===i.toLocaleLowerCase()):null;if(r)return vt({result:"Matched",chapterId:t.chapterId,sceneId:r.sceneId}),await hs(n.bookId,r.sceneId,t.chapterId,"Anchor","ChapterID Anchor"),!0;t&&!g?(rr="ChapterID Anchor",kr(null,b,"Chapter anchor"),vt({result:"Chapter matched",chapterId:b,sceneId:null})):g||(rr="ChapterID Anchor",vt({result:"Chapter anchor not found",chapterId:b,sceneId:null}))}const r=await lt(`/api/word-companion/books/${n.bookId}/resolve-scene`,{chapterTitle:u,sceneTitle:i});if(!r?.matched||!r.sceneId){const u=dt,f=Number.parseInt(r?.chapterId||"",10),e=Number.isInteger(o)&&o>0?o:null,n=Number.isInteger(f)&&f>0?f:e,i=!!r?.chapterMatched||!!n;return tt("Not found in PlotDirector"),dt=u,i&&(kr(null,n,n===e?"Chapter anchor":"Chapter title"),cf("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."),vt({result:"Not matched",chapterId:i?n:r?.chapterId,sceneId:r?.sceneId}),u||(i?(ke(),de("Scene not found in PlotDirector.")):(fs(),us("Chapter not found in PlotDirector."))),s(),!1}const f=dt;return vt({result:f?"Matched by title fallback":"Matched",chapterId:r.chapterId,sceneId:r.sceneId}),await hs(n.bookId,r.sceneId,r.chapterId,f?"Title fallback":"Title","Title Match"),f&&(dt=!0,t("Stored PlotDirector ID is invalid."),fr()),!0}catch(c){const n=dt;return f({lastError:e(c)}),tt("Not found in PlotDirector"),dt=n,t(n?"Stored PlotDirector ID is invalid.":"Unable to load PlotDirector scene data."),vt({result:"Error"}),!1}finally{s()}},dtt=n=>{const t=n.currentChapter?.title||"",i=n.currentScene?.title||"",r=n.currentChapter?.anchorId||null,u=n.currentScene?.anchorId||null;return ut(t,"chapter")!==ut(w,"chapter")||ut(i,"scene")!==ut(rt,"scene")||r!==b||u!==g},stt=async()=>{if(ru=null,go){rf=!0;i("Runtime update deferred; update already running.");return}if(!nf||!h()){br=!1;i("Runtime update skipped.");return}if(!kt||!it||!window.Word||typeof window.Word.run!="function"){br=!1;k("Manual");t("Automatic scene tracking is unavailable in this Word version. Use Refresh Current Scene.");return}if(!l){br=!1;k("Manual");t("Document structure is not cached yet. Use Refresh Current Scene.");return}const n=Date.now()-ov;if(br&&n{ru&&window.clearTimeout(ru),ru=window.setTimeout(stt,Math.max(0,n)),k("Waiting for typing to pause"),i("Runtime update scheduled.")},htt=(n="selection",t=ve)=>{if(br=!0,ov=Date.now(),le+=1,ae=null,af(),ro(),uf&&(pe=!0),ff&&(ef=!0),i(`${n} changed.`),go){rf=!0;k("Waiting for typing to pause");return}al(t)},ap=()=>{ru&&(window.clearTimeout(ru),ru=null),br=!1,rf=!1},vp=()=>{htt("Selection")},ctt=()=>{if(wh&&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:vp});wh=!1}catch(n){console.warn("Unable to remove Word selection tracking handler.",n)}},ltt=()=>{if(nf){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,vp,n=>{n?.status===window.Office.AsyncResultStatus.Succeeded?(wh=!0,k("Live")):(k("Manual"),t("Automatic scene tracking is unavailable in this Word version. Use Refresh Current Scene."))});window.addEventListener("beforeunload",ctt)}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.")}}},att=async()=>{if(!h()){t("Select a PlotDirector book first.");tt("Not detected");return}ap();av(!0);k("Updating...");try{await Promise.all([ol(!0),sl(!0),hl(!0)]);const n=await by();if(!n)return;if(li(!1),!rt){await ll();k("Current scene updated");return}await uo();k("Current scene updated")}finally{av(!1)}},yu=async(n,t)=>{if(l=null,su(),!n){ii=[];c(ot,"Select a project first");c(d,"Select a project first");hf("");v(s.bookId,null);pi();tt("Not detected");vf();return}c(ot,"Loading books...");hf("");tt("Not detected");vf("Select a book to analyse manuscript structure.");try{const r=await at(`/api/word-companion/projects/${n}/books`);if(ii=Array.isArray(r.books)?r.books:[],ii.length===0){c(ot,"No books available.");c(d,"No books available.");hf("No books available.");v(s.bookId,null);pi();ct("No books available.");return}ls(ot,"Choose a book",ii.map(n=>({...n,displayTitle:dr(n)})),"bookId","displayTitle");const i=ii.find(n=>n.bookId===t);i&&ot?(ot.value=String(i.bookId),v(s.bookId,i.bookId)):v(s.bookId,null);rd(i?.bookId||t);pi();tt(h()?"Not detected":"Not detected");vf(h()?"Ready to analyse manuscript structure.":"Select a book to analyse manuscript structure.");ct(au()?"Ready to scan manuscript structure.":"Select a Book to begin.")}catch{ii=[];c(ot,"Unable to load books.");c(d,"Unable to load books.");hf("Unable to load books.");v(s.bookId,null);pi();tt("Not detected");vf("Unable to load books.");ct("Unable to load books.")}},vtt=async()=>{c(p,"Loading projects...");c(ot,"Select a project first");c(et,"Loading projects...");c(d,"Select a project first");vc("");hf("");try{const t=await at("/api/word-companion/projects");if(ir=Array.isArray(t.projects)?t.projects:[],ir.length===0){c(p,"No projects available.");c(et,"No projects available.");vc("No projects available.");v(s.projectId,null);v(s.bookId,null);pi();ct("No projects available.");return}ls(p,"Choose a project",ir,"projectId","title");nl();const i=cs(s.projectId),n=ir.find(n=>n.projectId===i);n&&p?(p.value=String(n.projectId),et&&(et.value=String(n.projectId)),v(s.projectId,n.projectId),await yu(n.projectId,cs(s.bookId))):(v(s.projectId,null),v(s.bookId,null),pi(),ct("Select a Project and Book to begin."))}catch{ir=[];ii=[];c(p,"Unable to connect to PlotDirector.");c(ot,"Select a project first");c(et,"Unable to connect to PlotDirector.");c(d,"Select a project first");be("Unable to connect to PlotDirector.");vc("Unable to connect to PlotDirector.");v(s.projectId,null);v(s.bookId,null);pi();ct("Unable to connect to PlotDirector.")}};for(const n of nh)n.addEventListener("click",()=>cv(n.dataset.tabButton));lv();p?.addEventListener("change",async()=>{const n=Number.parseInt(p.value||"",10);yf();et&&Number.isInteger(n)&&n>0&&(et.value=String(n));v(s.projectId,Number.isInteger(n)&&n>0?n:null);v(s.bookId,null);tt("Not detected");vf();ct("Select a Book to begin.");await yu(n,null);pf()});ot?.addEventListener("change",()=>{const n=Number.parseInt(ot.value||"",10);yf();v(s.bookId,Number.isInteger(n)&&n>0?n:null);d&&Number.isInteger(n)&&n>0&&(d.value=String(n));pi();tt("Not detected");vf(h()?"Ready to analyse manuscript structure.":"Select a book to analyse manuscript structure.");ct(au()?"Ready to scan manuscript structure.":"Select a Book to begin.");pf()});et?.addEventListener("change",async()=>{const n=Number.parseInt(et.value||"",10);yf();p&&Number.isInteger(n)&&n>0&&(p.value=String(n));v(s.projectId,Number.isInteger(n)&&n>0?n:null);v(s.bookId,null);ct("Select a Book to begin.");await yu(n,null);pf()});d?.addEventListener("change",()=>{const n=Number.parseInt(d.value||"",10);yf();ot&&Number.isInteger(n)&&n>0&&(ot.value=String(n));v(s.bookId,Number.isInteger(n)&&n>0?n:null);pi();ct(au()?"Ready to scan manuscript structure.":"Select a Book to begin.");pf()});df?.addEventListener("input",ft);fo?.addEventListener("click",async()=>{const n=gr(),t=String(df?.value||"").trim();if(!n||!t){ft();return}fo.disabled=!0;ui("Creating Book...");try{const i=await lt(`/api/word-companion/projects/${n.projectId}/books`,{title:t});df&&(df.value="");p&&(p.value=String(n.projectId));await yu(n.projectId,i.bookId);d&&(d.value=String(i.bookId));ui("Ready to scan manuscript structure.")}catch(i){console.error("Unable to create Book from Word Companion.",i);ui(`Unable to create Book: ${e(i)}`)}finally{ft()}});sr?.addEventListener("click",wg);ih?.addEventListener("click",dg);eo?.addEventListener("click",gg);pl?.addEventListener("click",()=>ws("Character creation skipped."));wl?.addEventListener("click",()=>ws());dp?.addEventListener("click",()=>vu(!1));nw?.addEventListener("click",()=>ct("Import cancelled."));nu?.addEventListener("click",by);cr?.addEventListener("click",ky);rh?.addEventListener("click",yg);ob?.addEventListener("click",()=>ul(!0));sb?.addEventListener("click",()=>ul(!1));tr?.addEventListener("cancel",n=>{n.preventDefault(),ul(!1)});tu?.addEventListener("click",uo);oo?.addEventListener("click",att);ar?.addEventListener("click",()=>lp({source:"manual"}));di?.addEventListener("click",on);ai?.addEventListener("click",hn);lr?.addEventListener("click",rn);yt?.addEventListener("click",ln);pt?.addEventListener("click",an);co?.addEventListener("input",ep);si?.addEventListener("click",vn);ib?.addEventListener("click",op);fb?.addEventListener("click",()=>fl(!0));eb?.addEventListener("click",()=>fl(!1));nr?.addEventListener("cancel",n=>{n.preventDefault(),fl(!1)});wt?.addEventListener("click",pn);bt?.addEventListener("click",wn);so?.addEventListener("input",hp);oi?.addEventListener("click",bn);kw?.addEventListener("click",cp);nb?.addEventListener("click",()=>el(!0));tb?.addEventListener("click",()=>el(!1));gi?.addEventListener("cancel",n=>{n.preventDefault(),el(!1)});iu?.addEventListener("click",pg);const ytt=nf?vtt():Promise.resolve();if(nf||(c(p,"Please sign in to PlotDirector."),c(ot,"Please sign in to PlotDirector."),c(et,"Please sign in to PlotDirector."),c(d,"Please sign in to PlotDirector."),pi()),!window.Office||typeof window.Office.onReady!="function"){ac("Word Companion ready");t("Word document access is unavailable.");k("Manual");f({host:"Unavailable",platform:"Unavailable",ready:"No",wordApi:"No"});ny().catch(n=>{console.debug("Word Companion presence unavailable.",n)});return}kv().then(async()=>{ac("Word Companion ready"),await ytt,await tn(),await ny(),it?ltt():(t("Word document access is unavailable."),k("Manual"))}).catch(n=>{console.error("Office readiness check failed.",n),be("Unable to connect to PlotDirector."),ac("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 index 79848f6..7ff8acc 100644 --- a/PlotLine/wwwroot/js/word-companion-presence.js +++ b/PlotLine/wwwroot/js/word-companion-presence.js @@ -7,6 +7,9 @@ const labelFor = (status) => (status?.isConnected || status?.IsConnected ? "Connected" : status?.status || status?.Status || "Offline"); const field = (status, camel, pascal) => status?.[camel] ?? status?.[pascal] ?? null; + let companionConnected = false; + let companionDocumentOpen = false; + let currentScanStatus = document.querySelector("[data-onboarding-scan-panel]")?.dataset.scanStatus || "NotStarted"; const formatTime = (value) => { if (!value) { @@ -19,9 +22,12 @@ const applyStatus = (status) => { const connected = !!field(status, "isConnected", "IsConnected"); + const documentOpen = !!field(status, "documentOpen", "DocumentOpen"); const version = field(status, "companionVersion", "CompanionVersion") || "Waiting"; const documentName = field(status, "currentDocumentName", "CurrentDocumentName") || "No document reported yet"; const label = connected ? "Connected" : labelFor(status); + companionConnected = connected; + companionDocumentOpen = documentOpen; const title = [ `Word Companion: ${label}`, version && version !== "Waiting" ? `Version ${version}` : "", @@ -43,9 +49,17 @@ 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."; + if (!connected) { + node.textContent = "Open Word and start the Companion. This page will update automatically."; + } else if (!documentOpen) { + node.textContent = "Open your manuscript in Word, then the scan button will appear."; + } else if (currentScanStatus === "Complete") { + node.textContent = "Scan complete. Review the structure before anything is imported."; + } else if (currentScanStatus === "Running") { + node.textContent = "The Companion is scanning your manuscript now."; + } else { + node.textContent = "Your Companion is connected. Scan your manuscript when you are ready."; + } }); document.querySelectorAll("[data-word-companion-version]").forEach((node) => { node.textContent = version; @@ -61,6 +75,67 @@ ? "Your Word Companion is ready for this story setup." : "Open Word, start the Companion, and sign in with your PlotDirector account."; }); + updateScanButton(); + }; + + const numberText = (value) => { + const number = Number.parseInt(value || "0", 10); + return Number.isInteger(number) ? number.toLocaleString() : "0"; + }; + + const updateScanButton = () => { + document.querySelectorAll("[data-onboarding-scan-start]").forEach((button) => { + button.disabled = !companionConnected || !companionDocumentOpen || currentScanStatus === "Running"; + button.textContent = currentScanStatus === "Complete" ? "Scan again" : "Scan manuscript"; + }); + }; + + const applyScanState = (state) => { + if (!state) { + return; + } + + const status = field(state, "status", "Status") || "NotStarted"; + currentScanStatus = status; + const message = field(state, "message", "Message") || (status === "Complete" ? "We found:" : "Ready to scan"); + const percent = field(state, "percentComplete", "PercentComplete"); + const previewId = field(state, "previewID", "PreviewID") || field(state, "previewId", "PreviewId"); + const panel = document.querySelector("[data-onboarding-scan-panel]"); + if (panel) { + panel.dataset.scanStatus = status; + panel.classList.toggle("is-running", status === "Running"); + panel.classList.toggle("is-complete", status === "Complete"); + panel.classList.toggle("is-failed", status === "Failed"); + } + + document.querySelectorAll("[data-onboarding-scan-message]").forEach((node) => { + node.textContent = message; + }); + document.querySelectorAll("[data-onboarding-scan-percent]").forEach((node) => { + node.textContent = percent === null || percent === undefined ? "" : `${percent}%`; + }); + document.querySelectorAll("[data-onboarding-scan-progress]").forEach((node) => { + node.style.width = `${Math.max(0, Math.min(100, Number.parseInt(percent || "0", 10) || 0))}%`; + }); + document.querySelectorAll("[data-onboarding-scan-chapters]").forEach((node) => { + node.textContent = numberText(field(state, "chapterCount", "ChapterCount")); + }); + document.querySelectorAll("[data-onboarding-scan-scenes]").forEach((node) => { + node.textContent = numberText(field(state, "sceneCount", "SceneCount")); + }); + document.querySelectorAll("[data-onboarding-scan-characters]").forEach((node) => { + node.textContent = numberText(field(state, "characterCandidateCount", "CharacterCandidateCount")); + }); + document.querySelectorAll("[data-onboarding-scan-words]").forEach((node) => { + node.textContent = numberText(field(state, "totalWordCount", "TotalWordCount")); + }); + document.querySelectorAll("[data-onboarding-scan-review]").forEach((node) => { + const ready = status === "Complete" && previewId; + node.classList.toggle("disabled", !ready); + node.setAttribute("aria-disabled", String(!ready)); + node.href = ready ? `/onboarding/scan-review?previewId=${encodeURIComponent(previewId)}` : "#"; + }); + updateScanButton(); }; const connection = new signalR.HubConnectionBuilder() @@ -69,6 +144,10 @@ .build(); connection.on("WordCompanionPresenceChanged", applyStatus); + connection.on("OnboardingScanStateChanged", applyScanState); + connection.on("OnboardingScanProgress", applyScanState); + connection.on("OnboardingScanCompleted", applyScanState); + connection.on("OnboardingScanFailed", applyScanState); connection.onreconnecting(() => applyStatus({ status: "Connecting", isConnected: false })); connection.onreconnected(() => connection.invoke("WatchCompanionPresence").then(applyStatus).catch(() => {})); connection.onclose(() => applyStatus({ status: "Offline", isConnected: false })); @@ -77,4 +156,20 @@ .then(() => connection.invoke("WatchCompanionPresence")) .then(applyStatus) .catch(() => applyStatus({ status: "Offline", isConnected: false })); + + document.querySelectorAll("[data-onboarding-scan-start]").forEach((button) => { + button.addEventListener("click", async () => { + button.disabled = true; + applyScanState({ status: "Running", message: "Asking the Word Companion to scan...", percentComplete: 0 }); + try { + const state = await connection.invoke("StartOnboardingManuscriptScan"); + applyScanState(state); + } catch (error) { + applyScanState({ + status: "Failed", + message: error?.message || "The scan could not start. Reconnect the Word Companion and try again." + }); + } + }); + }); })();