Fix onboarding Word marker save acknowledgement
This commit is contained in:
parent
a6c870ab6d
commit
5a38560f47
@ -82,6 +82,7 @@ var tests = new (string Name, Action Test)[]
|
|||||||
("Scan review separates known matches from unresolved names", ScanReviewSeparatesKnownMatchesFromUnresolvedNames),
|
("Scan review separates known matches from unresolved names", ScanReviewSeparatesKnownMatchesFromUnresolvedNames),
|
||||||
("Story Intelligence uses persisted scene source after restart", StoryIntelligenceUsesPersistedSceneSourceAfterRestart),
|
("Story Intelligence uses persisted scene source after restart", StoryIntelligenceUsesPersistedSceneSourceAfterRestart),
|
||||||
("Word sync requires save acknowledgement", WordSyncRequiresSaveAcknowledgement),
|
("Word sync requires save acknowledgement", WordSyncRequiresSaveAcknowledgement),
|
||||||
|
("Word sync retry resumes existing structure", WordSyncRetryResumesExistingStructure),
|
||||||
("Story Intelligence enriches existing structural scenes", StoryIntelligenceEnrichesExistingStructuralScenes),
|
("Story Intelligence enriches existing structural scenes", StoryIntelligenceEnrichesExistingStructuralScenes),
|
||||||
("Phase 21T repairs durable character understanding", Phase21TRepairsDurableCharacterUnderstanding),
|
("Phase 21T repairs durable character understanding", Phase21TRepairsDurableCharacterUnderstanding),
|
||||||
("Phase 21U extracts mandatory character appearance fixtures", Phase21UExtractsMandatoryCharacterAppearanceFixtures),
|
("Phase 21U extracts mandatory character appearance fixtures", Phase21UExtractsMandatoryCharacterAppearanceFixtures),
|
||||||
@ -1921,14 +1922,39 @@ static void WordSyncRequiresSaveAcknowledgement()
|
|||||||
var companion = File.ReadAllText(Path.Combine(root, "wwwroot/js/word-companion-host.js"));
|
var companion = File.ReadAllText(Path.Combine(root, "wwwroot/js/word-companion-host.js"));
|
||||||
var hub = File.ReadAllText(Path.Combine(root, "Hubs/WordCompanionFollowHub.cs"));
|
var hub = File.ReadAllText(Path.Combine(root, "Hubs/WordCompanionFollowHub.cs"));
|
||||||
var presence = File.ReadAllText(Path.Combine(root, "wwwroot/js/word-companion-presence.js"));
|
var presence = File.ReadAllText(Path.Combine(root, "wwwroot/js/word-companion-presence.js"));
|
||||||
var migration = File.ReadAllText(Path.Combine(root, "Sql/149_Phase24_StructuralImportMarkerSync.sql"));
|
var migration = File.ReadAllText(Path.Combine(root, "Sql/158_Phase25C_OnboardingMarkerSaveStates.sql"));
|
||||||
|
|
||||||
Assert(companion.Contains("saveWordDocumentIfAvailable", StringComparison.Ordinal), "Companion should save Word before acknowledging marker sync.");
|
Assert(companion.Contains("saveWordDocumentIfAvailable", StringComparison.Ordinal), "Companion should save Word before acknowledging marker sync.");
|
||||||
|
Assert(companion.Contains("markersChanged", StringComparison.Ordinal), "Companion should distinguish marker insertion from an idempotent no-change retry.");
|
||||||
|
Assert(companion.Contains("SaveNotRequired", StringComparison.Ordinal), "Companion should acknowledge marker sync when no Word save is required.");
|
||||||
|
Assert(companion.Contains("UserConfirmedSave", StringComparison.Ordinal), "Companion should support an explicit manual save acknowledgement.");
|
||||||
|
Assert(companion.Contains("RequireOnboardingBuildManualSave", StringComparison.Ordinal), "Companion should report manual-save-required state without throwing a generic SignalR error.");
|
||||||
Assert(companion.Contains("CompleteOnboardingBuildMarkerSync", StringComparison.Ordinal), "Companion should acknowledge successful marker sync.");
|
Assert(companion.Contains("CompleteOnboardingBuildMarkerSync", StringComparison.Ordinal), "Companion should acknowledge successful marker sync.");
|
||||||
Assert(companion.Contains("FailOnboardingBuildMarkerSync", StringComparison.Ordinal), "Companion should report failed marker sync.");
|
Assert(companion.Contains("FailOnboardingBuildMarkerSync", StringComparison.Ordinal), "Companion should report failed marker sync.");
|
||||||
|
Assert(companion.Contains("officeRuntimeDiagnostics", StringComparison.Ordinal), "Companion should capture Word host diagnostics for save failures.");
|
||||||
|
Assert(hub.Contains("MarkerSyncClientResult", StringComparison.Ordinal), "Server should accept marker sync diagnostics.");
|
||||||
Assert(hub.Contains("MarkMarkerSyncCompletedAsync", StringComparison.Ordinal), "Server should persist completed marker sync.");
|
Assert(hub.Contains("MarkMarkerSyncCompletedAsync", StringComparison.Ordinal), "Server should persist completed marker sync.");
|
||||||
|
Assert(hub.Contains("MarkMarkerSyncUserSaveRequiredAsync", StringComparison.Ordinal), "Server should persist manual-save-required marker sync.");
|
||||||
Assert(presence.Contains("OnboardingBuildMarkerSyncCompleted", StringComparison.Ordinal), "Browser should wait for marker sync completion before redirecting.");
|
Assert(presence.Contains("OnboardingBuildMarkerSyncCompleted", StringComparison.Ordinal), "Browser should wait for marker sync completion before redirecting.");
|
||||||
Assert(migration.Contains("MarkerSyncCompletedUtc", StringComparison.Ordinal), "Database should persist marker sync completion time.");
|
Assert(migration.Contains("MarkerSyncCompletedUtc", StringComparison.Ordinal), "Database should persist marker sync completion time.");
|
||||||
|
Assert(migration.Contains("UserSaveRequired", StringComparison.Ordinal), "Database should persist manual-save-required marker sync state.");
|
||||||
|
Assert(migration.Contains("SaveNotRequired", StringComparison.Ordinal), "Database should persist no-change marker sync state.");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void WordSyncRetryResumesExistingStructure()
|
||||||
|
{
|
||||||
|
var root = Path.Combine(AppContext.BaseDirectory, "../../../../PlotLine");
|
||||||
|
var onboarding = File.ReadAllText(Path.Combine(root, "Services/OnboardingService.cs"));
|
||||||
|
var repository = File.ReadAllText(Path.Combine(root, "Data/OnboardingBuildRepository.cs"));
|
||||||
|
var migration = File.ReadAllText(Path.Combine(root, "Sql/158_Phase25C_OnboardingMarkerSaveStates.sql"));
|
||||||
|
|
||||||
|
var resumeCheck = onboarding.IndexOf("await builds.GetByPreviewAsync(userId, preview.PreviewID)", StringComparison.Ordinal);
|
||||||
|
var sceneDetection = onboarding.IndexOf("\"Detecting scene boundaries...\"", StringComparison.Ordinal);
|
||||||
|
Assert(resumeCheck >= 0 && sceneDetection > resumeCheck, "Existing structural builds should be checked before rerunning scene boundary AI.");
|
||||||
|
Assert(onboarding.Contains("BuildExistingStructureResultAsync", StringComparison.Ordinal), "Retry should rebuild marker instructions from persisted structure.");
|
||||||
|
Assert(onboarding.Contains("BuildSceneMarkerInstructionsFromSources", StringComparison.Ordinal), "Retry should use stored SceneID paragraph positions for marker sync.");
|
||||||
|
Assert(repository.Contains("Onboarding_ManuscriptBuild_GetByPreview", StringComparison.Ordinal), "Existing build lookup should use a stored procedure.");
|
||||||
|
Assert(migration.Contains("CREATE OR ALTER PROCEDURE dbo.Onboarding_ManuscriptBuild_GetByPreview", StringComparison.Ordinal), "Forward migration should provide the existing build lookup procedure.");
|
||||||
}
|
}
|
||||||
|
|
||||||
static void StoryIntelligenceEnrichesExistingStructuralScenes()
|
static void StoryIntelligenceEnrichesExistingStructuralScenes()
|
||||||
|
|||||||
@ -8,9 +8,11 @@ namespace PlotLine.Data;
|
|||||||
public interface IOnboardingBuildRepository
|
public interface IOnboardingBuildRepository
|
||||||
{
|
{
|
||||||
Task<OnboardingManuscriptBuildResult?> BuildAsync(int userId, OnboardingManuscriptBuildRequest request);
|
Task<OnboardingManuscriptBuildResult?> BuildAsync(int userId, OnboardingManuscriptBuildRequest request);
|
||||||
|
Task<OnboardingManuscriptBuildResult?> GetByPreviewAsync(int userId, Guid previewId);
|
||||||
Task MarkMarkerSyncSentAsync(int buildId);
|
Task MarkMarkerSyncSentAsync(int buildId);
|
||||||
Task MarkMarkerSyncCompletedAsync(int buildId);
|
Task MarkMarkerSyncCompletedAsync(int buildId, string markerStatus, string message);
|
||||||
Task MarkMarkerSyncFailedAsync(int buildId, string message);
|
Task MarkMarkerSyncUserSaveRequiredAsync(int buildId, string message);
|
||||||
|
Task MarkMarkerSyncFailedAsync(int buildId, string message, string? diagnostics = null);
|
||||||
Task SaveStructuralChapterSourceAsync(StructuralChapterSourceSaveRequest request);
|
Task SaveStructuralChapterSourceAsync(StructuralChapterSourceSaveRequest request);
|
||||||
Task SaveSceneManuscriptSourceAsync(SceneManuscriptSourceSaveRequest request);
|
Task SaveSceneManuscriptSourceAsync(SceneManuscriptSourceSaveRequest request);
|
||||||
Task SaveSceneStructuralSummaryAsync(int sceneId, string structuralSummary);
|
Task SaveSceneStructuralSummaryAsync(int sceneId, string structuralSummary);
|
||||||
@ -38,6 +40,22 @@ public sealed class OnboardingBuildRepository(ISqlConnectionFactory connectionFa
|
|||||||
},
|
},
|
||||||
commandType: CommandType.StoredProcedure);
|
commandType: CommandType.StoredProcedure);
|
||||||
|
|
||||||
|
return await ReadBuildResultAsync(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<OnboardingManuscriptBuildResult?> GetByPreviewAsync(int userId, Guid previewId)
|
||||||
|
{
|
||||||
|
using var connection = connectionFactory.CreateConnection();
|
||||||
|
using var result = await connection.QueryMultipleAsync(
|
||||||
|
"dbo.Onboarding_ManuscriptBuild_GetByPreview",
|
||||||
|
new { UserID = userId, PreviewID = previewId },
|
||||||
|
commandType: CommandType.StoredProcedure);
|
||||||
|
|
||||||
|
return await ReadBuildResultAsync(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<OnboardingManuscriptBuildResult?> ReadBuildResultAsync(SqlMapper.GridReader result)
|
||||||
|
{
|
||||||
var header = await result.ReadSingleOrDefaultAsync<OnboardingManuscriptBuildResult>();
|
var header = await result.ReadSingleOrDefaultAsync<OnboardingManuscriptBuildResult>();
|
||||||
if (header is null)
|
if (header is null)
|
||||||
{
|
{
|
||||||
@ -78,21 +96,30 @@ public sealed class OnboardingBuildRepository(ISqlConnectionFactory connectionFa
|
|||||||
commandType: CommandType.StoredProcedure);
|
commandType: CommandType.StoredProcedure);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task MarkMarkerSyncCompletedAsync(int buildId)
|
public async Task MarkMarkerSyncCompletedAsync(int buildId, string markerStatus, string message)
|
||||||
{
|
{
|
||||||
using var connection = connectionFactory.CreateConnection();
|
using var connection = connectionFactory.CreateConnection();
|
||||||
await connection.ExecuteAsync(
|
await connection.ExecuteAsync(
|
||||||
"dbo.Onboarding_ManuscriptBuild_MarkerSyncCompleted",
|
"dbo.Onboarding_ManuscriptBuild_MarkerSyncCompleted",
|
||||||
new { BuildID = buildId },
|
new { BuildID = buildId, MarkerStatus = markerStatus, Message = message },
|
||||||
commandType: CommandType.StoredProcedure);
|
commandType: CommandType.StoredProcedure);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task MarkMarkerSyncFailedAsync(int buildId, string message)
|
public async Task MarkMarkerSyncUserSaveRequiredAsync(int buildId, string message)
|
||||||
|
{
|
||||||
|
using var connection = connectionFactory.CreateConnection();
|
||||||
|
await connection.ExecuteAsync(
|
||||||
|
"dbo.Onboarding_ManuscriptBuild_MarkerSyncUserSaveRequired",
|
||||||
|
new { BuildID = buildId, Message = message },
|
||||||
|
commandType: CommandType.StoredProcedure);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task MarkMarkerSyncFailedAsync(int buildId, string message, string? diagnostics = null)
|
||||||
{
|
{
|
||||||
using var connection = connectionFactory.CreateConnection();
|
using var connection = connectionFactory.CreateConnection();
|
||||||
await connection.ExecuteAsync(
|
await connection.ExecuteAsync(
|
||||||
"dbo.Onboarding_ManuscriptBuild_MarkerSyncFailed",
|
"dbo.Onboarding_ManuscriptBuild_MarkerSyncFailed",
|
||||||
new { BuildID = buildId, Message = message },
|
new { BuildID = buildId, Message = message, Diagnostics = diagnostics },
|
||||||
commandType: CommandType.StoredProcedure);
|
commandType: CommandType.StoredProcedure);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -4,6 +4,7 @@ using PlotLine.Data;
|
|||||||
using PlotLine.Models;
|
using PlotLine.Models;
|
||||||
using PlotLine.Services;
|
using PlotLine.Services;
|
||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
namespace PlotLine.Hubs;
|
namespace PlotLine.Hubs;
|
||||||
|
|
||||||
@ -262,23 +263,74 @@ public sealed class WordCompanionFollowHub(
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task CompleteOnboardingBuildMarkerSync(int buildId)
|
public async Task CompleteOnboardingBuildMarkerSync(int buildId, MarkerSyncClientResult? result = null)
|
||||||
{
|
{
|
||||||
var userId = RequireUserId();
|
var userId = RequireUserId();
|
||||||
await onboardingBuilds.MarkMarkerSyncCompletedAsync(buildId);
|
var markerStatus = NormaliseCompletedMarkerStatus(result?.MarkerStatus);
|
||||||
|
var message = string.IsNullOrWhiteSpace(result?.Message)
|
||||||
|
? markerStatus switch
|
||||||
|
{
|
||||||
|
"SaveNotRequired" => "Scene markers already existed. No Word save was required.",
|
||||||
|
"UserConfirmedSave" => "Scene markers added; user confirmed the Word document was saved.",
|
||||||
|
_ => "Scene markers added and document saved."
|
||||||
|
}
|
||||||
|
: result.Message;
|
||||||
|
|
||||||
|
logger.LogInformation(
|
||||||
|
"Onboarding marker sync completed. UserID={UserID} BuildID={BuildID} MarkerStatus={MarkerStatus} MarkersChanged={MarkersChanged} SaveState={SaveState} SaveApi={SaveApi} Host={Host} Platform={Platform} Version={Version} DocumentGuid={DocumentGuid}",
|
||||||
|
userId,
|
||||||
|
buildId,
|
||||||
|
markerStatus,
|
||||||
|
result?.MarkersChanged,
|
||||||
|
result?.SaveState,
|
||||||
|
result?.SaveApi,
|
||||||
|
result?.Host,
|
||||||
|
result?.Platform,
|
||||||
|
result?.Version,
|
||||||
|
result?.DocumentGuid);
|
||||||
|
|
||||||
|
await onboardingBuilds.MarkMarkerSyncCompletedAsync(buildId, markerStatus, message);
|
||||||
await Clients.Group(PresenceGroup(userId)).SendAsync("OnboardingBuildProgress", new OnboardingBuildProgress
|
await Clients.Group(PresenceGroup(userId)).SendAsync("OnboardingBuildProgress", new OnboardingBuildProgress
|
||||||
{
|
{
|
||||||
Status = "Complete",
|
Status = "Complete",
|
||||||
Message = "Manuscript structure has been created and saved. Word may now be closed; keep this browser workflow open to review characters.",
|
Message = "Manuscript structure has been created and saved. Word may now be closed; keep this browser workflow open to review characters.",
|
||||||
PercentComplete = 100
|
PercentComplete = 100
|
||||||
});
|
});
|
||||||
await Clients.Group(PresenceGroup(userId)).SendAsync("OnboardingBuildMarkerSyncCompleted", new { buildId });
|
await Clients.Group(PresenceGroup(userId)).SendAsync("OnboardingBuildMarkerSyncCompleted", new { buildId, markerStatus });
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task FailOnboardingBuildMarkerSync(int buildId, string message)
|
public async Task RequireOnboardingBuildManualSave(int buildId, string message, MarkerSyncClientResult? result = null)
|
||||||
{
|
{
|
||||||
var userId = RequireUserId();
|
var userId = RequireUserId();
|
||||||
await onboardingBuilds.MarkMarkerSyncFailedAsync(buildId, message);
|
logger.LogWarning(
|
||||||
|
"Onboarding marker sync requires manual Word save. UserID={UserID} BuildID={BuildID} Message={Message} Diagnostics={Diagnostics}",
|
||||||
|
userId,
|
||||||
|
buildId,
|
||||||
|
message,
|
||||||
|
SerialiseMarkerDiagnostics(result));
|
||||||
|
|
||||||
|
await onboardingBuilds.MarkMarkerSyncUserSaveRequiredAsync(buildId, message);
|
||||||
|
await Clients.Group(PresenceGroup(userId)).SendAsync("OnboardingBuildProgress", new OnboardingBuildProgress
|
||||||
|
{
|
||||||
|
Status = "Running",
|
||||||
|
Message = string.IsNullOrWhiteSpace(message)
|
||||||
|
? "Scene markers have been added. Save the document in Word, then continue the import again."
|
||||||
|
: message,
|
||||||
|
PercentComplete = 90
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task FailOnboardingBuildMarkerSync(int buildId, string message, MarkerSyncClientResult? result = null)
|
||||||
|
{
|
||||||
|
var userId = RequireUserId();
|
||||||
|
logger.LogWarning(
|
||||||
|
"Onboarding marker sync failed. UserID={UserID} BuildID={BuildID} Message={Message} Diagnostics={Diagnostics}",
|
||||||
|
userId,
|
||||||
|
buildId,
|
||||||
|
message,
|
||||||
|
SerialiseMarkerDiagnostics(result));
|
||||||
|
|
||||||
|
await onboardingBuilds.MarkMarkerSyncFailedAsync(buildId, message, SerialiseMarkerDiagnostics(result));
|
||||||
await Clients.Group(PresenceGroup(userId)).SendAsync("OnboardingBuildProgress", new OnboardingBuildProgress
|
await Clients.Group(PresenceGroup(userId)).SendAsync("OnboardingBuildProgress", new OnboardingBuildProgress
|
||||||
{
|
{
|
||||||
Status = "Failed",
|
Status = "Failed",
|
||||||
@ -343,6 +395,12 @@ public sealed class WordCompanionFollowHub(
|
|||||||
=> string.Equals(state.WritingSoftware, WritingSoftwareValues.MicrosoftWord, StringComparison.Ordinal)
|
=> string.Equals(state.WritingSoftware, WritingSoftwareValues.MicrosoftWord, StringComparison.Ordinal)
|
||||||
|| state.UsesWordCompanion == true;
|
|| state.UsesWordCompanion == true;
|
||||||
|
|
||||||
|
private static string NormaliseCompletedMarkerStatus(string? markerStatus)
|
||||||
|
=> markerStatus is "SaveNotRequired" or "UserConfirmedSave" ? markerStatus : "Completed";
|
||||||
|
|
||||||
|
private static string? SerialiseMarkerDiagnostics(MarkerSyncClientResult? result)
|
||||||
|
=> result is null ? null : JsonSerializer.Serialize(result);
|
||||||
|
|
||||||
private static ManuscriptScanPreview NormalisePreviewContext(ManuscriptScanPreview preview, int userId, UserOnboardingState state)
|
private static ManuscriptScanPreview NormalisePreviewContext(ManuscriptScanPreview preview, int userId, UserOnboardingState state)
|
||||||
{
|
{
|
||||||
return new()
|
return new()
|
||||||
@ -366,3 +424,20 @@ public sealed class WordCompanionFollowHub(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public sealed class MarkerSyncClientResult
|
||||||
|
{
|
||||||
|
public string? MarkerStatus { get; init; }
|
||||||
|
public string? Message { get; init; }
|
||||||
|
public bool? MarkersChanged { get; init; }
|
||||||
|
public string? SaveState { get; init; }
|
||||||
|
public string? SaveApi { get; init; }
|
||||||
|
public string? Host { get; init; }
|
||||||
|
public string? Platform { get; init; }
|
||||||
|
public string? Version { get; init; }
|
||||||
|
public string? DocumentGuid { get; init; }
|
||||||
|
public string? DocumentUrl { get; init; }
|
||||||
|
public string? ErrorName { get; init; }
|
||||||
|
public string? ErrorCode { get; init; }
|
||||||
|
public string? ErrorMessage { get; init; }
|
||||||
|
}
|
||||||
|
|||||||
@ -235,6 +235,16 @@ public sealed class OnboardingService(
|
|||||||
throw new InvalidOperationException("Review the scan and choose Save and continue before preparing chapters.");
|
throw new InvalidOperationException("Review the scan and choose Save and continue before preparing chapters.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var existingBuild = await scanPreviews.GetBuildResultAsync(userId, preview.PreviewID)
|
||||||
|
?? await builds.GetByPreviewAsync(userId, preview.PreviewID);
|
||||||
|
if (existingBuild is not null && existingBuild.SceneMappings.Count > 0)
|
||||||
|
{
|
||||||
|
var resumed = await BuildExistingStructureResultAsync(userId, preview, review, existingBuild);
|
||||||
|
await scanPreviews.SaveBuildResultAsync(userId, resumed);
|
||||||
|
await PublishBuildProgress(userId, preview.PreviewID, "Resuming Word marker sync. No scene detection was rerun.", 90, progress, resumed);
|
||||||
|
return resumed;
|
||||||
|
}
|
||||||
|
|
||||||
await PublishBuildProgress(userId, preview.PreviewID, "Preparing approved chapters for scene detection...", 10, progress);
|
await PublishBuildProgress(userId, preview.PreviewID, "Preparing approved chapters for scene detection...", 10, progress);
|
||||||
ValidateScanStructure(review, preview);
|
ValidateScanStructure(review, preview);
|
||||||
|
|
||||||
@ -348,6 +358,42 @@ public sealed class OnboardingService(
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<OnboardingManuscriptBuildResult> BuildExistingStructureResultAsync(
|
||||||
|
int userId,
|
||||||
|
ManuscriptScanPreview preview,
|
||||||
|
ManuscriptScanReviewDecision review,
|
||||||
|
OnboardingManuscriptBuildResult existingBuild)
|
||||||
|
{
|
||||||
|
var sceneSources = await builds.ListSceneManuscriptSourcesByBookAsync(preview.BookID);
|
||||||
|
var characterCandidates = WordCompanionService.DiscoverCoreImportCharacterCandidates(sceneSources);
|
||||||
|
await scanPreviews.SaveCharacterCandidatesAsync(userId, preview.PreviewID, characterCandidates);
|
||||||
|
|
||||||
|
return new OnboardingManuscriptBuildResult
|
||||||
|
{
|
||||||
|
BuildID = existingBuild.BuildID,
|
||||||
|
PreviewID = existingBuild.PreviewID,
|
||||||
|
ProjectID = existingBuild.ProjectID,
|
||||||
|
BookID = existingBuild.BookID,
|
||||||
|
ManuscriptDocumentID = existingBuild.ManuscriptDocumentID,
|
||||||
|
Status = existingBuild.Status,
|
||||||
|
MarkerStatus = existingBuild.MarkerStatus,
|
||||||
|
ChaptersCreated = existingBuild.ChaptersCreated,
|
||||||
|
ScenesCreated = existingBuild.ScenesCreated,
|
||||||
|
CharactersCreated = existingBuild.CharactersCreated,
|
||||||
|
CharactersReused = existingBuild.CharactersReused,
|
||||||
|
SceneAppearancesCreated = existingBuild.SceneAppearancesCreated,
|
||||||
|
AlreadyBuilt = true,
|
||||||
|
Message = "Manuscript structure already exists. Updating and saving Word markers...",
|
||||||
|
ChapterMappings = existingBuild.ChapterMappings,
|
||||||
|
SceneMappings = existingBuild.SceneMappings,
|
||||||
|
CharacterMappings = existingBuild.CharacterMappings,
|
||||||
|
ChapterMarkerInstructions = BuildChapterMarkerInstructions(preview, existingBuild.ChapterMappings),
|
||||||
|
SceneMarkerInstructions = BuildSceneMarkerInstructionsFromSources(sceneSources, existingBuild.SceneMappings),
|
||||||
|
SceneStructure = BuildSceneStructureSummaryFromSources(review.Chapters.Where(chapter => chapter.Include).ToList(), sceneSources),
|
||||||
|
MarkerWarning = existingBuild.MarkerWarning
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<OnboardingManuscriptBuildResult?> GetBuildResultAsync(Guid previewId)
|
public async Task<OnboardingManuscriptBuildResult?> GetBuildResultAsync(Guid previewId)
|
||||||
=> await scanPreviews.GetBuildResultAsync(RequireUserId(), previewId);
|
=> await scanPreviews.GetBuildResultAsync(RequireUserId(), previewId);
|
||||||
|
|
||||||
@ -514,6 +560,31 @@ public sealed class OnboardingService(
|
|||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static IReadOnlyList<OnboardingBuildMarkerInstruction> BuildSceneMarkerInstructionsFromSources(
|
||||||
|
IReadOnlyList<SceneManuscriptSourceRecord> sources,
|
||||||
|
IReadOnlyList<OnboardingBuildSceneMap> mappings)
|
||||||
|
{
|
||||||
|
var sourcesBySceneId = sources
|
||||||
|
.Where(source => source.WordStartParagraphIndex.HasValue)
|
||||||
|
.ToDictionary(source => source.SceneID);
|
||||||
|
return mappings
|
||||||
|
.Where(mapping => sourcesBySceneId.ContainsKey(mapping.SceneID))
|
||||||
|
.Select(mapping =>
|
||||||
|
{
|
||||||
|
var source = sourcesBySceneId[mapping.SceneID];
|
||||||
|
return new OnboardingBuildMarkerInstruction
|
||||||
|
{
|
||||||
|
TemporaryKey = mapping.TemporarySceneKey,
|
||||||
|
EntityID = mapping.SceneID,
|
||||||
|
WordParagraphIndex = source.WordStartParagraphIndex!.Value,
|
||||||
|
MarkerTag = $"PD-SCENE-{mapping.SceneID}",
|
||||||
|
MarkerTitle = "PlotDirector Scene",
|
||||||
|
MarkerPrefix = "PD-SCENE"
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
private static IReadOnlyList<OnboardingSceneStructureSummary> BuildSceneStructureSummary(
|
private static IReadOnlyList<OnboardingSceneStructureSummary> BuildSceneStructureSummary(
|
||||||
IReadOnlyList<ManuscriptScanChapterReviewDecision> chapters,
|
IReadOnlyList<ManuscriptScanChapterReviewDecision> chapters,
|
||||||
IReadOnlyList<ManuscriptStructureSceneBoundary> scenes)
|
IReadOnlyList<ManuscriptStructureSceneBoundary> scenes)
|
||||||
@ -545,6 +616,44 @@ public sealed class OnboardingService(
|
|||||||
})
|
})
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
|
private static IReadOnlyList<OnboardingSceneStructureSummary> BuildSceneStructureSummaryFromSources(
|
||||||
|
IReadOnlyList<ManuscriptScanChapterReviewDecision> chapters,
|
||||||
|
IReadOnlyList<SceneManuscriptSourceRecord> sources)
|
||||||
|
{
|
||||||
|
var byChapter = sources
|
||||||
|
.GroupBy(source => source.ChapterID)
|
||||||
|
.OrderBy(group => group.Min(source => source.SourceStartParagraph ?? int.MaxValue))
|
||||||
|
.ThenBy(group => group.Key)
|
||||||
|
.ToList();
|
||||||
|
return chapters
|
||||||
|
.OrderBy(chapter => chapter.ChapterNumber)
|
||||||
|
.Select((chapter, index) =>
|
||||||
|
{
|
||||||
|
var chapterSources = byChapter.ElementAtOrDefault(index)?
|
||||||
|
.OrderBy(source => source.SourceStartParagraph ?? int.MaxValue)
|
||||||
|
.ThenBy(source => source.SceneID)
|
||||||
|
.ToList() ?? [];
|
||||||
|
return new OnboardingSceneStructureSummary
|
||||||
|
{
|
||||||
|
TemporaryChapterKey = chapter.TemporaryChapterKey,
|
||||||
|
ChapterNumber = chapter.ChapterNumber,
|
||||||
|
ChapterTitle = chapter.Title,
|
||||||
|
SceneCount = chapterSources.Count,
|
||||||
|
WordCount = chapterSources.Sum(scene => scene.SourceWordCount),
|
||||||
|
Scenes = chapterSources.Select((scene, sceneIndex) => new OnboardingSceneStructureDetail
|
||||||
|
{
|
||||||
|
SceneNumberWithinChapter = sceneIndex + 1,
|
||||||
|
Title = $"Scene {sceneIndex + 1:N0}",
|
||||||
|
WordCount = scene.SourceWordCount,
|
||||||
|
StructuralSummary = scene.StructuralSummary,
|
||||||
|
StartParagraph = scene.SourceStartParagraph ?? 0,
|
||||||
|
EndParagraph = scene.SourceEndParagraph ?? 0
|
||||||
|
}).ToList()
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
private async Task SaveStructuralChapterSourcesAsync(ManuscriptScanPreview preview, OnboardingManuscriptBuildResult result)
|
private async Task SaveStructuralChapterSourcesAsync(ManuscriptScanPreview preview, OnboardingManuscriptBuildResult result)
|
||||||
{
|
{
|
||||||
var chapterIds = result.ChapterMappings.ToDictionary(item => item.TemporaryChapterKey, item => item.ChapterID, StringComparer.Ordinal);
|
var chapterIds = result.ChapterMappings.ToDictionary(item => item.TemporaryChapterKey, item => item.ChapterID, StringComparer.Ordinal);
|
||||||
|
|||||||
109
PlotLine/Sql/158_Phase25C_OnboardingMarkerSaveStates.sql
Normal file
109
PlotLine/Sql/158_Phase25C_OnboardingMarkerSaveStates.sql
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
SET ANSI_NULLS ON;
|
||||||
|
GO
|
||||||
|
SET QUOTED_IDENTIFIER ON;
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE OR ALTER PROCEDURE dbo.Onboarding_ManuscriptBuild_GetByPreview
|
||||||
|
@UserID int,
|
||||||
|
@PreviewID uniqueidentifier
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
|
||||||
|
DECLARE @BuildID int;
|
||||||
|
SELECT @BuildID = OnboardingManuscriptBuildID
|
||||||
|
FROM dbo.OnboardingManuscriptBuilds
|
||||||
|
WHERE PreviewID = @PreviewID
|
||||||
|
AND UserID = @UserID;
|
||||||
|
|
||||||
|
IF @BuildID IS NULL
|
||||||
|
RETURN;
|
||||||
|
|
||||||
|
SELECT OnboardingManuscriptBuildID AS BuildID, PreviewID, ProjectID, BookID, ManuscriptDocumentID, Status, MarkerStatus,
|
||||||
|
ChaptersCreated, ScenesCreated, CharactersCreated, CharactersReused, SceneAppearancesCreated,
|
||||||
|
CAST(1 AS bit) AS AlreadyBuilt,
|
||||||
|
CASE MarkerStatus
|
||||||
|
WHEN N'Completed' THEN N'This manuscript structure has already been built and saved.'
|
||||||
|
WHEN N'SaveNotRequired' THEN N'This manuscript structure has already been built; the expected Word markers were already present.'
|
||||||
|
WHEN N'UserConfirmedSave' THEN N'This manuscript structure has already been built and the Word save was confirmed.'
|
||||||
|
WHEN N'UserSaveRequired' THEN N'This manuscript structure has already been built. Save the Word document, then continue the import again.'
|
||||||
|
ELSE N'This manuscript structure has already been built into PlotDirector.'
|
||||||
|
END AS Message
|
||||||
|
FROM dbo.OnboardingManuscriptBuilds
|
||||||
|
WHERE OnboardingManuscriptBuildID = @BuildID;
|
||||||
|
|
||||||
|
SELECT TemporaryChapterKey, ChapterID
|
||||||
|
FROM dbo.OnboardingManuscriptBuildChapterMap
|
||||||
|
WHERE OnboardingManuscriptBuildID = @BuildID
|
||||||
|
ORDER BY TemporaryChapterKey;
|
||||||
|
|
||||||
|
SELECT TemporarySceneKey, SceneID
|
||||||
|
FROM dbo.OnboardingManuscriptBuildSceneMap
|
||||||
|
WHERE OnboardingManuscriptBuildID = @BuildID
|
||||||
|
ORDER BY TemporarySceneKey;
|
||||||
|
|
||||||
|
SELECT TemporaryCharacterKey, CharacterID, Created
|
||||||
|
FROM dbo.OnboardingManuscriptBuildCharacterMap
|
||||||
|
WHERE OnboardingManuscriptBuildID = @BuildID
|
||||||
|
ORDER BY TemporaryCharacterKey;
|
||||||
|
END;
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE OR ALTER PROCEDURE dbo.Onboarding_ManuscriptBuild_MarkerSyncCompleted
|
||||||
|
@BuildID int,
|
||||||
|
@MarkerStatus nvarchar(40) = N'Completed',
|
||||||
|
@Message nvarchar(500) = NULL
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
|
||||||
|
IF @MarkerStatus NOT IN (N'Completed', N'SaveNotRequired', N'UserConfirmedSave')
|
||||||
|
SET @MarkerStatus = N'Completed';
|
||||||
|
|
||||||
|
UPDATE dbo.OnboardingManuscriptBuilds
|
||||||
|
SET MarkerStatus = @MarkerStatus,
|
||||||
|
MarkerSyncMessage = COALESCE(
|
||||||
|
LEFT(NULLIF(LTRIM(RTRIM(@Message)), N''), 500),
|
||||||
|
CASE @MarkerStatus
|
||||||
|
WHEN N'SaveNotRequired' THEN N'Scene markers already existed. No Word save was required.'
|
||||||
|
WHEN N'UserConfirmedSave' THEN N'Scene markers added; user confirmed the Word document was saved.'
|
||||||
|
ELSE N'Scene markers added and document saved.'
|
||||||
|
END),
|
||||||
|
MarkerSyncCompletedUtc = SYSUTCDATETIME(),
|
||||||
|
UpdatedUtc = SYSUTCDATETIME()
|
||||||
|
WHERE OnboardingManuscriptBuildID = @BuildID;
|
||||||
|
END;
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE OR ALTER PROCEDURE dbo.Onboarding_ManuscriptBuild_MarkerSyncUserSaveRequired
|
||||||
|
@BuildID int,
|
||||||
|
@Message nvarchar(500) = NULL
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
|
||||||
|
UPDATE dbo.OnboardingManuscriptBuilds
|
||||||
|
SET MarkerStatus = N'UserSaveRequired',
|
||||||
|
MarkerSyncMessage = COALESCE(
|
||||||
|
LEFT(NULLIF(LTRIM(RTRIM(@Message)), N''), 500),
|
||||||
|
N'Scene markers have been added. Save the document in Word, then continue the import again.'),
|
||||||
|
UpdatedUtc = SYSUTCDATETIME()
|
||||||
|
WHERE OnboardingManuscriptBuildID = @BuildID;
|
||||||
|
END;
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE OR ALTER PROCEDURE dbo.Onboarding_ManuscriptBuild_MarkerSyncFailed
|
||||||
|
@BuildID int,
|
||||||
|
@Message nvarchar(500) = NULL,
|
||||||
|
@Diagnostics nvarchar(max) = NULL
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
|
||||||
|
UPDATE dbo.OnboardingManuscriptBuilds
|
||||||
|
SET MarkerStatus = N'Failed',
|
||||||
|
MarkerSyncMessage = LEFT(NULLIF(LTRIM(RTRIM(@Message)), N''), 500),
|
||||||
|
UpdatedUtc = SYSUTCDATETIME()
|
||||||
|
WHERE OnboardingManuscriptBuildID = @BuildID;
|
||||||
|
END;
|
||||||
|
GO
|
||||||
@ -1771,18 +1771,81 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const officeRuntimeDiagnostics = () => {
|
||||||
|
const diagnostics = window.Office?.context?.diagnostics || {};
|
||||||
|
const document = window.Office?.context?.document;
|
||||||
|
return {
|
||||||
|
host: diagnostics.host || window.Office?.context?.host || officeHost || "",
|
||||||
|
platform: diagnostics.platform || window.Office?.context?.platform || officePlatform || "",
|
||||||
|
version: diagnostics.version || "",
|
||||||
|
documentUrl: document?.url || "",
|
||||||
|
documentGuid: currentDocumentGuid()
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const officeAsyncErrorDetails = (error) => ({
|
||||||
|
errorName: error?.name || "",
|
||||||
|
errorCode: error?.code || "",
|
||||||
|
errorMessage: error?.message || errorText(error)
|
||||||
|
});
|
||||||
|
|
||||||
const saveWordDocumentIfAvailable = () => new Promise((resolve) => {
|
const saveWordDocumentIfAvailable = () => new Promise((resolve) => {
|
||||||
const document = window.Office?.context?.document;
|
const document = window.Office?.context?.document;
|
||||||
if (!document || typeof document.saveAsync !== "function") {
|
if (!document || typeof document.saveAsync !== "function") {
|
||||||
resolve(false);
|
resolve({
|
||||||
|
saveState: "Unsupported",
|
||||||
|
saveApi: "Office.context.document.saveAsync",
|
||||||
|
...officeRuntimeDiagnostics(),
|
||||||
|
errorMessage: "Office.context.document.saveAsync is not available in this Word host."
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
document.saveAsync((result) => {
|
document.saveAsync((result) => {
|
||||||
resolve(result.status === window.Office.AsyncResultStatus.Succeeded);
|
const succeeded = result.status === window.Office.AsyncResultStatus.Succeeded;
|
||||||
|
resolve({
|
||||||
|
saveState: succeeded ? "Saved" : "Failed",
|
||||||
|
saveApi: "Office.context.document.saveAsync",
|
||||||
|
...officeRuntimeDiagnostics(),
|
||||||
|
...(succeeded ? {} : officeAsyncErrorDetails(result.error))
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const invokeMarkerSyncFailure = async (buildId, message, diagnostics = {}) => {
|
||||||
|
if (Number.isInteger(buildId) && buildId > 0 && companionPresenceConnection) {
|
||||||
|
try {
|
||||||
|
await companionPresenceConnection.invoke("FailOnboardingBuildMarkerSync", buildId, message, diagnostics);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("Unable to report onboarding marker sync failure.", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const invokeMarkerSyncManualSaveRequired = async (buildId, message, diagnostics = {}) => {
|
||||||
|
if (Number.isInteger(buildId) && buildId > 0 && companionPresenceConnection) {
|
||||||
|
try {
|
||||||
|
await companionPresenceConnection.invoke("RequireOnboardingBuildManualSave", buildId, message, diagnostics);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("Unable to report onboarding manual save requirement.", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const invokeMarkerSyncCompleted = async (buildId, markerStatus, message, diagnostics = {}) => {
|
||||||
|
if (Number.isInteger(buildId) && buildId > 0 && companionPresenceConnection) {
|
||||||
|
try {
|
||||||
|
await companionPresenceConnection.invoke("CompleteOnboardingBuildMarkerSync", buildId, {
|
||||||
|
markerStatus,
|
||||||
|
message,
|
||||||
|
...diagnostics
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("Unable to report onboarding marker sync completion.", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const ensureDocumentGuid = () => {
|
const ensureDocumentGuid = () => {
|
||||||
if (firstRunDocumentBinding?.documentGuid) {
|
if (firstRunDocumentBinding?.documentGuid) {
|
||||||
return firstRunDocumentBinding.documentGuid;
|
return firstRunDocumentBinding.documentGuid;
|
||||||
@ -2368,10 +2431,6 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const updateOnboardingBuildMarkers = async (result) => {
|
const updateOnboardingBuildMarkers = async (result) => {
|
||||||
if (!wordHostAvailable || !window.Word || typeof window.Word.run !== "function") {
|
|
||||||
throw new Error("Word document access is unavailable.");
|
|
||||||
}
|
|
||||||
|
|
||||||
const buildId = Number.parseInt(result?.buildID || result?.BuildID || "0", 10);
|
const buildId = Number.parseInt(result?.buildID || result?.BuildID || "0", 10);
|
||||||
const chapterMap = new Map((result?.chapterMappings || result?.ChapterMappings || [])
|
const chapterMap = new Map((result?.chapterMappings || result?.ChapterMappings || [])
|
||||||
.map((item) => [item.temporaryChapterKey || item.TemporaryChapterKey, item.chapterID || item.ChapterID]));
|
.map((item) => [item.temporaryChapterKey || item.TemporaryChapterKey, item.chapterID || item.ChapterID]));
|
||||||
@ -2384,6 +2443,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if (!wordHostAvailable || !window.Word || typeof window.Word.run !== "function") {
|
||||||
|
throw new Error("Word document access is unavailable.");
|
||||||
|
}
|
||||||
|
|
||||||
|
let markersChanged = false;
|
||||||
await window.Word.run(async (context) => {
|
await window.Word.run(async (context) => {
|
||||||
const paragraphs = context.document.body.paragraphs;
|
const paragraphs = context.document.body.paragraphs;
|
||||||
paragraphs.load("items/text,items/style,items/styleBuiltIn");
|
paragraphs.load("items/text,items/style,items/styleBuiltIn");
|
||||||
@ -2396,7 +2460,7 @@
|
|||||||
const title = instruction.markerTitle || instruction.MarkerTitle || "PlotDirector Chapter";
|
const title = instruction.markerTitle || instruction.MarkerTitle || "PlotDirector Chapter";
|
||||||
const prefix = instruction.markerPrefix || instruction.MarkerPrefix || "PD-CHAPTER";
|
const prefix = instruction.markerPrefix || instruction.MarkerPrefix || "PD-CHAPTER";
|
||||||
if (tag && Number.isInteger(index) && paragraphs.items[index]) {
|
if (tag && Number.isInteger(index) && paragraphs.items[index]) {
|
||||||
ensureAnchorControl(paragraphs.items[index], title, tag, prefix);
|
markersChanged = ensureAnchorControl(paragraphs.items[index], title, tag, prefix) || markersChanged;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2406,7 +2470,7 @@
|
|||||||
const title = instruction.markerTitle || instruction.MarkerTitle || "PlotDirector Scene";
|
const title = instruction.markerTitle || instruction.MarkerTitle || "PlotDirector Scene";
|
||||||
const prefix = instruction.markerPrefix || instruction.MarkerPrefix || "PD-SCENE";
|
const prefix = instruction.markerPrefix || instruction.MarkerPrefix || "PD-SCENE";
|
||||||
if (tag && Number.isInteger(index) && paragraphs.items[index]) {
|
if (tag && Number.isInteger(index) && paragraphs.items[index]) {
|
||||||
ensureAnchorControl(paragraphs.items[index], title, tag, prefix);
|
markersChanged = ensureAnchorControl(paragraphs.items[index], title, tag, prefix) || markersChanged;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -2414,7 +2478,7 @@
|
|||||||
const chapterId = chapterMap.get(chapter.temporaryChapterKey);
|
const chapterId = chapterMap.get(chapter.temporaryChapterKey);
|
||||||
const index = Number.parseInt(chapter.startPosition, 10);
|
const index = Number.parseInt(chapter.startPosition, 10);
|
||||||
if (chapterId && Number.isInteger(index) && paragraphs.items[index]) {
|
if (chapterId && Number.isInteger(index) && paragraphs.items[index]) {
|
||||||
ensureAnchorControl(paragraphs.items[index], "PlotDirector Chapter", `PD-CHAPTER-${chapterId}`, "PD-CHAPTER");
|
markersChanged = ensureAnchorControl(paragraphs.items[index], "PlotDirector Chapter", `PD-CHAPTER-${chapterId}`, "PD-CHAPTER") || markersChanged;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2422,7 +2486,7 @@
|
|||||||
const sceneId = sceneMap.get(scene.temporarySceneKey);
|
const sceneId = sceneMap.get(scene.temporarySceneKey);
|
||||||
const index = Number.parseInt(scene.startPosition, 10);
|
const index = Number.parseInt(scene.startPosition, 10);
|
||||||
if (sceneId && Number.isInteger(index) && paragraphs.items[index]) {
|
if (sceneId && Number.isInteger(index) && paragraphs.items[index]) {
|
||||||
ensureAnchorControl(paragraphs.items[index], "PlotDirector Scene", `PD-SCENE-${sceneId}`, "PD-SCENE");
|
markersChanged = ensureAnchorControl(paragraphs.items[index], "PlotDirector Scene", `PD-SCENE-${sceneId}`, "PD-SCENE") || markersChanged;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2430,20 +2494,71 @@
|
|||||||
await context.sync();
|
await context.sync();
|
||||||
});
|
});
|
||||||
|
|
||||||
const saved = await saveWordDocumentIfAvailable();
|
await inspectPlotDirectorMarkers().catch(() => null);
|
||||||
if (!saved) {
|
if (!markersChanged) {
|
||||||
throw new Error("Word markers were inserted, but the document could not be saved.");
|
const diagnostics = {
|
||||||
|
markersChanged: false,
|
||||||
|
saveState: "NotRequired",
|
||||||
|
saveApi: "none",
|
||||||
|
...officeRuntimeDiagnostics()
|
||||||
|
};
|
||||||
|
console.info("Onboarding marker sync completed without Word document changes.", diagnostics);
|
||||||
|
await invokeMarkerSyncCompleted(
|
||||||
|
buildId,
|
||||||
|
"SaveNotRequired",
|
||||||
|
"Scene markers already existed. No Word save was required.",
|
||||||
|
diagnostics);
|
||||||
|
setDocumentMessage("Scene markers already existed. Word may now be closed.");
|
||||||
|
setDiagnostics({ lastError: "-" });
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await inspectPlotDirectorMarkers().catch(() => null);
|
const saveResult = await saveWordDocumentIfAvailable();
|
||||||
if (Number.isInteger(buildId) && buildId > 0) {
|
const saveDiagnostics = {
|
||||||
await companionPresenceConnection.invoke("CompleteOnboardingBuildMarkerSync", buildId);
|
markersChanged: true,
|
||||||
|
...saveResult
|
||||||
|
};
|
||||||
|
console.info("Onboarding marker sync Word save result.", saveDiagnostics);
|
||||||
|
if (saveResult.saveState === "Saved") {
|
||||||
|
await invokeMarkerSyncCompleted(
|
||||||
|
buildId,
|
||||||
|
"Completed",
|
||||||
|
"Scene markers added and document saved.",
|
||||||
|
saveDiagnostics);
|
||||||
|
setDocumentMessage("Scene markers added and document saved. Word may now be closed.");
|
||||||
|
setDiagnostics({ lastError: "-" });
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const manualMessage = "Scene markers have been added. Save the document in Word now, then click OK to continue.";
|
||||||
|
setDocumentMessage(manualMessage);
|
||||||
|
setDiagnostics({ lastError: saveResult.errorMessage || "Word document save requires manual confirmation." });
|
||||||
|
const userConfirmedSave = window.confirm(`${manualMessage}\n\nClick Cancel if the document was not saved.`);
|
||||||
|
if (userConfirmedSave) {
|
||||||
|
await invokeMarkerSyncCompleted(
|
||||||
|
buildId,
|
||||||
|
"UserConfirmedSave",
|
||||||
|
"Scene markers added; user confirmed the Word document was saved.",
|
||||||
|
saveDiagnostics);
|
||||||
|
setDocumentMessage("Scene markers added and save confirmed. Word may now be closed.");
|
||||||
|
setDiagnostics({ lastError: "-" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await invokeMarkerSyncManualSaveRequired(
|
||||||
|
buildId,
|
||||||
|
"Scene markers were added, but Word save still needs confirmation. Save the document in Word, then continue the import again.",
|
||||||
|
saveDiagnostics);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (Number.isInteger(buildId) && buildId > 0) {
|
console.error("Unable to update onboarding Word markers.", error);
|
||||||
await companionPresenceConnection.invoke("FailOnboardingBuildMarkerSync", buildId, errorText(error));
|
setDocumentMessage("Unable to update PlotDirector scene markers.");
|
||||||
}
|
setDiagnostics({ lastError: errorText(error) });
|
||||||
throw error;
|
await invokeMarkerSyncFailure(buildId, errorText(error), {
|
||||||
|
markersChanged: null,
|
||||||
|
saveState: "Failed",
|
||||||
|
...officeRuntimeDiagnostics(),
|
||||||
|
errorMessage: errorText(error)
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -4336,11 +4451,15 @@
|
|||||||
|
|
||||||
const ensureAnchorControl = (paragraph, title, tag, prefix) => {
|
const ensureAnchorControl = (paragraph, title, tag, prefix) => {
|
||||||
const existing = findAnchorControl(paragraph, prefix);
|
const existing = findAnchorControl(paragraph, prefix);
|
||||||
|
if (existing && existing.tag === tag && existing.title === title) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
const control = existing || paragraph.getRange().insertContentControl();
|
const control = existing || paragraph.getRange().insertContentControl();
|
||||||
control.title = title;
|
control.title = title;
|
||||||
control.tag = tag;
|
control.tag = tag;
|
||||||
control.appearance = "Hidden";
|
control.appearance = "Hidden";
|
||||||
return control;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
const attachResolvedPlotDirectorIds = async (successMessage = "PlotDirector IDs attached successfully.") => {
|
const attachResolvedPlotDirectorIds = async (successMessage = "PlotDirector IDs attached successfully.") => {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user