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),
|
||||
("Story Intelligence uses persisted scene source after restart", StoryIntelligenceUsesPersistedSceneSourceAfterRestart),
|
||||
("Word sync requires save acknowledgement", WordSyncRequiresSaveAcknowledgement),
|
||||
("Word sync retry resumes existing structure", WordSyncRetryResumesExistingStructure),
|
||||
("Story Intelligence enriches existing structural scenes", StoryIntelligenceEnrichesExistingStructuralScenes),
|
||||
("Phase 21T repairs durable character understanding", Phase21TRepairsDurableCharacterUnderstanding),
|
||||
("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 hub = File.ReadAllText(Path.Combine(root, "Hubs/WordCompanionFollowHub.cs"));
|
||||
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("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("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("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(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()
|
||||
|
||||
@ -8,9 +8,11 @@ namespace PlotLine.Data;
|
||||
public interface IOnboardingBuildRepository
|
||||
{
|
||||
Task<OnboardingManuscriptBuildResult?> BuildAsync(int userId, OnboardingManuscriptBuildRequest request);
|
||||
Task<OnboardingManuscriptBuildResult?> GetByPreviewAsync(int userId, Guid previewId);
|
||||
Task MarkMarkerSyncSentAsync(int buildId);
|
||||
Task MarkMarkerSyncCompletedAsync(int buildId);
|
||||
Task MarkMarkerSyncFailedAsync(int buildId, string message);
|
||||
Task MarkMarkerSyncCompletedAsync(int buildId, string markerStatus, string message);
|
||||
Task MarkMarkerSyncUserSaveRequiredAsync(int buildId, string message);
|
||||
Task MarkMarkerSyncFailedAsync(int buildId, string message, string? diagnostics = null);
|
||||
Task SaveStructuralChapterSourceAsync(StructuralChapterSourceSaveRequest request);
|
||||
Task SaveSceneManuscriptSourceAsync(SceneManuscriptSourceSaveRequest request);
|
||||
Task SaveSceneStructuralSummaryAsync(int sceneId, string structuralSummary);
|
||||
@ -38,6 +40,22 @@ public sealed class OnboardingBuildRepository(ISqlConnectionFactory connectionFa
|
||||
},
|
||||
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>();
|
||||
if (header is null)
|
||||
{
|
||||
@ -78,21 +96,30 @@ public sealed class OnboardingBuildRepository(ISqlConnectionFactory connectionFa
|
||||
commandType: CommandType.StoredProcedure);
|
||||
}
|
||||
|
||||
public async Task MarkMarkerSyncCompletedAsync(int buildId)
|
||||
public async Task MarkMarkerSyncCompletedAsync(int buildId, string markerStatus, string message)
|
||||
{
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
await connection.ExecuteAsync(
|
||||
"dbo.Onboarding_ManuscriptBuild_MarkerSyncCompleted",
|
||||
new { BuildID = buildId },
|
||||
new { BuildID = buildId, MarkerStatus = markerStatus, Message = message },
|
||||
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();
|
||||
await connection.ExecuteAsync(
|
||||
"dbo.Onboarding_ManuscriptBuild_MarkerSyncFailed",
|
||||
new { BuildID = buildId, Message = message },
|
||||
new { BuildID = buildId, Message = message, Diagnostics = diagnostics },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
}
|
||||
|
||||
|
||||
@ -4,6 +4,7 @@ using PlotLine.Data;
|
||||
using PlotLine.Models;
|
||||
using PlotLine.Services;
|
||||
using System.Security.Claims;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace PlotLine.Hubs;
|
||||
|
||||
@ -262,23 +263,74 @@ public sealed class WordCompanionFollowHub(
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task CompleteOnboardingBuildMarkerSync(int buildId)
|
||||
public async Task CompleteOnboardingBuildMarkerSync(int buildId, MarkerSyncClientResult? result = null)
|
||||
{
|
||||
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
|
||||
{
|
||||
Status = "Complete",
|
||||
Message = "Manuscript structure has been created and saved. Word may now be closed; keep this browser workflow open to review characters.",
|
||||
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();
|
||||
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
|
||||
{
|
||||
Status = "Failed",
|
||||
@ -343,6 +395,12 @@ public sealed class WordCompanionFollowHub(
|
||||
=> string.Equals(state.WritingSoftware, WritingSoftwareValues.MicrosoftWord, StringComparison.Ordinal)
|
||||
|| 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)
|
||||
{
|
||||
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.");
|
||||
}
|
||||
|
||||
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);
|
||||
ValidateScanStructure(review, preview);
|
||||
|
||||
@ -348,6 +358,42 @@ public sealed class OnboardingService(
|
||||
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)
|
||||
=> await scanPreviews.GetBuildResultAsync(RequireUserId(), previewId);
|
||||
|
||||
@ -514,6 +560,31 @@ public sealed class OnboardingService(
|
||||
.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(
|
||||
IReadOnlyList<ManuscriptScanChapterReviewDecision> chapters,
|
||||
IReadOnlyList<ManuscriptStructureSceneBoundary> scenes)
|
||||
@ -545,6 +616,44 @@ public sealed class OnboardingService(
|
||||
})
|
||||
.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)
|
||||
{
|
||||
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,17 +1771,80 @@
|
||||
});
|
||||
});
|
||||
|
||||
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 document = window.Office?.context?.document;
|
||||
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;
|
||||
}
|
||||
|
||||
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 = () => {
|
||||
if (firstRunDocumentBinding?.documentGuid) {
|
||||
@ -2368,10 +2431,6 @@
|
||||
};
|
||||
|
||||
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 chapterMap = new Map((result?.chapterMappings || result?.ChapterMappings || [])
|
||||
.map((item) => [item.temporaryChapterKey || item.TemporaryChapterKey, item.chapterID || item.ChapterID]));
|
||||
@ -2384,6 +2443,11 @@
|
||||
}
|
||||
|
||||
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) => {
|
||||
const paragraphs = context.document.body.paragraphs;
|
||||
paragraphs.load("items/text,items/style,items/styleBuiltIn");
|
||||
@ -2396,7 +2460,7 @@
|
||||
const title = instruction.markerTitle || instruction.MarkerTitle || "PlotDirector Chapter";
|
||||
const prefix = instruction.markerPrefix || instruction.MarkerPrefix || "PD-CHAPTER";
|
||||
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 prefix = instruction.markerPrefix || instruction.MarkerPrefix || "PD-SCENE";
|
||||
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 {
|
||||
@ -2414,7 +2478,7 @@
|
||||
const chapterId = chapterMap.get(chapter.temporaryChapterKey);
|
||||
const index = Number.parseInt(chapter.startPosition, 10);
|
||||
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 index = Number.parseInt(scene.startPosition, 10);
|
||||
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();
|
||||
});
|
||||
|
||||
const saved = await saveWordDocumentIfAvailable();
|
||||
if (!saved) {
|
||||
throw new Error("Word markers were inserted, but the document could not be saved.");
|
||||
await inspectPlotDirectorMarkers().catch(() => null);
|
||||
if (!markersChanged) {
|
||||
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);
|
||||
if (Number.isInteger(buildId) && buildId > 0) {
|
||||
await companionPresenceConnection.invoke("CompleteOnboardingBuildMarkerSync", buildId);
|
||||
const saveResult = await saveWordDocumentIfAvailable();
|
||||
const saveDiagnostics = {
|
||||
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) {
|
||||
if (Number.isInteger(buildId) && buildId > 0) {
|
||||
await companionPresenceConnection.invoke("FailOnboardingBuildMarkerSync", buildId, errorText(error));
|
||||
}
|
||||
throw error;
|
||||
console.error("Unable to update onboarding Word markers.", error);
|
||||
setDocumentMessage("Unable to update PlotDirector scene markers.");
|
||||
setDiagnostics({ lastError: errorText(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 existing = findAnchorControl(paragraph, prefix);
|
||||
if (existing && existing.tag === tag && existing.title === title) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const control = existing || paragraph.getRange().insertContentControl();
|
||||
control.title = title;
|
||||
control.tag = tag;
|
||||
control.appearance = "Hidden";
|
||||
return control;
|
||||
return true;
|
||||
};
|
||||
|
||||
const attachResolvedPlotDirectorIds = async (successMessage = "PlotDirector IDs attached successfully.") => {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user