Require Word marker save acknowledgement
This commit is contained in:
parent
fbe8e60d59
commit
adf93a075c
@ -14,6 +14,7 @@ public sealed class WordCompanionFollowHub(
|
||||
IOnboardingRepository onboarding,
|
||||
IManuscriptScanPreviewStore scanStore,
|
||||
IOnboardingService onboardingService,
|
||||
IOnboardingBuildRepository onboardingBuilds,
|
||||
ILogger<WordCompanionFollowHub> logger) : Hub
|
||||
{
|
||||
public override async Task OnConnectedAsync()
|
||||
@ -223,13 +224,55 @@ public sealed class WordCompanionFollowHub(
|
||||
var companionConnectionId = await presence.GetCompanionConnectionIdAsync(userId);
|
||||
if (!string.IsNullOrWhiteSpace(companionConnectionId))
|
||||
{
|
||||
await onboardingBuilds.MarkMarkerSyncSentAsync(result.BuildID);
|
||||
await Clients.Client(companionConnectionId).SendAsync("UpdateOnboardingBuildMarkers", result);
|
||||
}
|
||||
else
|
||||
{
|
||||
await onboardingBuilds.MarkMarkerSyncFailedAsync(result.BuildID, "The Word Companion disconnected before marker sync could be sent.");
|
||||
await Clients.Group(PresenceGroup(userId)).SendAsync("OnboardingBuildProgress", new OnboardingBuildProgress
|
||||
{
|
||||
Status = "Failed",
|
||||
Message = "The Word Companion disconnected before PlotDirector could update and save Word markers.",
|
||||
PercentComplete = 90,
|
||||
ChaptersCreated = result.ChaptersCreated,
|
||||
ScenesCreated = result.ScenesCreated,
|
||||
CharactersCreated = result.CharactersCreated + result.CharactersReused,
|
||||
SceneAppearancesCreated = result.SceneAppearancesCreated,
|
||||
PreviewID = previewId
|
||||
});
|
||||
}
|
||||
|
||||
await Clients.Group(PresenceGroup(userId)).SendAsync("OnboardingBuildCompleted", result);
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task CompleteOnboardingBuildMarkerSync(int buildId)
|
||||
{
|
||||
var userId = RequireUserId();
|
||||
await onboardingBuilds.MarkMarkerSyncCompletedAsync(buildId);
|
||||
await Clients.Group(PresenceGroup(userId)).SendAsync("OnboardingBuildProgress", new OnboardingBuildProgress
|
||||
{
|
||||
Status = "Complete",
|
||||
Message = "Manuscript structure has been created and saved. Word and this browser page may now be closed.",
|
||||
PercentComplete = 100
|
||||
});
|
||||
await Clients.Group(PresenceGroup(userId)).SendAsync("OnboardingBuildMarkerSyncCompleted", new { buildId });
|
||||
}
|
||||
|
||||
public async Task FailOnboardingBuildMarkerSync(int buildId, string message)
|
||||
{
|
||||
var userId = RequireUserId();
|
||||
await onboardingBuilds.MarkMarkerSyncFailedAsync(buildId, message);
|
||||
await Clients.Group(PresenceGroup(userId)).SendAsync("OnboardingBuildProgress", new OnboardingBuildProgress
|
||||
{
|
||||
Status = "Failed",
|
||||
Message = string.IsNullOrWhiteSpace(message)
|
||||
? "Word marker sync failed. Keep Word open and try again."
|
||||
: message,
|
||||
PercentComplete = 90
|
||||
});
|
||||
}
|
||||
|
||||
public override async Task OnDisconnectedAsync(Exception? exception)
|
||||
{
|
||||
int? disconnectedUserId = null;
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
@model OnboardingManuscriptBuildResult
|
||||
@{
|
||||
ViewData["Title"] = "Chapters prepared";
|
||||
ViewData["Title"] = "Manuscript imported";
|
||||
}
|
||||
|
||||
<section class="onboarding-shell" aria-labelledby="build-complete-title">
|
||||
<div class="onboarding-panel onboarding-review-panel">
|
||||
<ol class="onboarding-stepper onboarding-stepper--complete" aria-label="Story setup journey">
|
||||
@foreach (var label in new[] { "Welcome", "Writing Preferences", "Project", "Book", "Connect Word", "Scan", "Review", "Prepare Chapters", "Complete" })
|
||||
@foreach (var label in new[] { "Welcome", "Writing Preferences", "Project", "Book", "Connect Word", "Scan", "Review", "Create Structure", "Complete" })
|
||||
{
|
||||
<li class="is-complete">
|
||||
<span></span>
|
||||
@ -15,12 +15,12 @@
|
||||
}
|
||||
</ol>
|
||||
<div class="onboarding-copy">
|
||||
<p class="eyebrow">Prepare Chapters</p>
|
||||
<p class="eyebrow">Core Import Complete</p>
|
||||
<div class="onboarding-success-heading">
|
||||
<span aria-hidden="true">✓</span>
|
||||
<h1 id="build-complete-title">Your chapters are ready.</h1>
|
||||
<h1 id="build-complete-title">Your manuscript has been imported.</h1>
|
||||
</div>
|
||||
<p>@(Model.AlreadyBuilt ? "These chapters have already been prepared in PlotDirector, so nothing was duplicated." : "PlotDirector prepared the approved chapters. Story Intelligence will create scenes after you review the analysis.")</p>
|
||||
<p>@(Model.AlreadyBuilt ? "This manuscript structure has already been created in PlotDirector, so nothing was duplicated." : "PlotDirector created the approved chapters and scenes, updated the Word markers, and saved the manuscript. Word and this browser page may now be closed.")</p>
|
||||
@if (!string.IsNullOrWhiteSpace(Model.MarkerWarning))
|
||||
{
|
||||
<p class="text-warning">@Model.MarkerWarning</p>
|
||||
@ -33,7 +33,7 @@
|
||||
<strong>@Model.ChaptersCreated</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Scenes created now</span>
|
||||
<span>Scenes</span>
|
||||
<strong>@Model.ScenesCreated</strong>
|
||||
</div>
|
||||
<div>
|
||||
@ -47,12 +47,13 @@
|
||||
</div>
|
||||
|
||||
<section class="onboarding-complete-next">
|
||||
<h2>Next: analyse manuscript</h2>
|
||||
<p>Story Intelligence will detect scenes from the approved chapter text. You will review the results before PlotDirector creates scenes.</p>
|
||||
<h2>Optional: analyse manuscript</h2>
|
||||
<p>Story Intelligence can now enrich the existing scenes with summaries, context, relationships, knowledge, timeline details and metrics. You can analyse now or return later.</p>
|
||||
</section>
|
||||
|
||||
<div class="onboarding-actions">
|
||||
<a class="btn btn-primary" asp-controller="Onboarding" asp-action="StoryIntelligence">Analyse manuscript</a>
|
||||
<a class="btn btn-primary" asp-controller="Onboarding" asp-action="StoryIntelligence">Analyse my book</a>
|
||||
<a class="btn btn-outline-secondary" asp-controller="Projects" asp-action="Details" asp-route-id="@Model.ProjectID">Not now</a>
|
||||
<a class="btn btn-outline-primary" asp-controller="Projects" asp-action="Details" asp-route-id="@Model.ProjectID">Open Project Overview</a>
|
||||
<a class="btn btn-outline-secondary" asp-controller="Writer" asp-action="Index" asp-route-projectId="@Model.ProjectID">Open Writer Workspace</a>
|
||||
</div>
|
||||
|
||||
@ -300,8 +300,8 @@
|
||||
{
|
||||
<div class="onboarding-build-panel" data-onboarding-build-panel>
|
||||
<div>
|
||||
<p class="onboarding-scan-next">Review saved. PlotDirector can now analyse the approved chapters.</p>
|
||||
<p class="onboarding-build-message" data-onboarding-build-message>Next: analyse the manuscript and review suggested scenes.</p>
|
||||
<p class="onboarding-scan-next">Review saved. PlotDirector can now import the approved manuscript structure.</p>
|
||||
<p class="onboarding-build-message" data-onboarding-build-message>Next: detect scene boundaries, create PlotDirector scenes, then update and save Word markers.</p>
|
||||
</div>
|
||||
<strong data-onboarding-build-percent></strong>
|
||||
<div class="onboarding-scan-progress onboarding-build-progress" aria-hidden="true">
|
||||
@ -319,7 +319,12 @@
|
||||
</form>
|
||||
@if (Model.IsMicrosoftWordPath && string.Equals(Model.ScanState.ReviewStatus, ManuscriptScanReviewStatuses.ReadyToImport, StringComparison.Ordinal))
|
||||
{
|
||||
<a class="btn btn-primary" asp-controller="Onboarding" asp-action="StoryIntelligence">Analyse manuscript</a>
|
||||
<button class="btn btn-primary"
|
||||
type="button"
|
||||
data-onboarding-build-start
|
||||
data-preview-id="@Model.ScanState.PreviewID">
|
||||
Import manuscript
|
||||
</button>
|
||||
}
|
||||
else if (Model.IsMicrosoftWordPath && Model.ScanState.IsComplete)
|
||||
{
|
||||
|
||||
@ -17,7 +17,7 @@
|
||||
<div class="onboarding-copy story-live-hero">
|
||||
<p class="eyebrow">Analyse manuscript</p>
|
||||
<h1 id="story-progress-title">Reading your manuscript</h1>
|
||||
<p>PlotDirector is analysing your chapters and preparing scenes for review. Keep Word and the Companion open until PlotDirector has finished creating the scene structure you need.</p>
|
||||
<p>PlotDirector is enriching the imported scenes with summaries, context, relationships, knowledge, timeline details and metrics.</p>
|
||||
</div>
|
||||
|
||||
@if (TempData["OnboardingStoryIntelligenceError"] is string error)
|
||||
@ -30,7 +30,7 @@
|
||||
}
|
||||
|
||||
<section class="alert alert-info">
|
||||
PlotDirector will keep analysing your manuscript on the server. Do not close Word yet if you need this manuscript to receive PlotDirector scene markers for the writing workspace.
|
||||
PlotDirector will keep analysing your imported scenes on the server. The manuscript structure and Word markers are already saved, so Word does not need to remain open for this analysis.
|
||||
</section>
|
||||
|
||||
<section class="story-live-progress-card" aria-label="Import progress">
|
||||
|
||||
@ -2199,13 +2199,19 @@
|
||||
return scene;
|
||||
};
|
||||
|
||||
const addWords = (text, words) => {
|
||||
const addWords = (paragraph, text, words) => {
|
||||
if (!chapter || !scene || words <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
chapter.wordCount += words;
|
||||
chapter.chapterTextParagraphs.push(text);
|
||||
chapter.paragraphMap = chapter.paragraphMap || [];
|
||||
chapter.paragraphMap.push({
|
||||
structuralParagraphOrdinal: chapter.chapterTextParagraphs.length,
|
||||
wordParagraphIndex: Number.isInteger(paragraph?.index) ? paragraph.index : -1,
|
||||
text
|
||||
});
|
||||
scene.wordCount += words;
|
||||
if (!scene.openingTextPreview && text && !sceneSeparatorTexts.has(text)) {
|
||||
scene.openingTextPreview = text.length > 180 ? `${text.slice(0, 177)}...` : text;
|
||||
@ -2233,6 +2239,12 @@
|
||||
totalWordCount += words;
|
||||
chapter.wordCount += words;
|
||||
chapter.chapterTextParagraphs.push(text);
|
||||
chapter.paragraphMap = chapter.paragraphMap || [];
|
||||
chapter.paragraphMap.push({
|
||||
structuralParagraphOrdinal: chapter.chapterTextParagraphs.length,
|
||||
wordParagraphIndex: Number.isInteger(paragraph?.index) ? paragraph.index : -1,
|
||||
text
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@ -2249,7 +2261,7 @@
|
||||
if (isBuiltInHeading(paragraph, 2)) {
|
||||
startScene(paragraph, text);
|
||||
totalWordCount += words;
|
||||
addWords(text, words);
|
||||
addWords(paragraph, text, words);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -2259,7 +2271,7 @@
|
||||
}
|
||||
|
||||
totalWordCount += words;
|
||||
addWords(text, words);
|
||||
addWords(paragraph, text, words);
|
||||
});
|
||||
|
||||
if (totalWordCount === 0) {
|
||||
@ -2391,20 +2403,48 @@
|
||||
};
|
||||
|
||||
const updateOnboardingBuildMarkers = async (result) => {
|
||||
if (!lastOnboardingScanPreview || !wordHostAvailable || !window.Word || typeof window.Word.run !== "function") {
|
||||
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]));
|
||||
const sceneMap = new Map((result?.sceneMappings || result?.SceneMappings || [])
|
||||
.map((item) => [item.temporarySceneKey || item.TemporarySceneKey, item.sceneID || item.SceneID]));
|
||||
const chapterInstructions = result?.chapterMarkerInstructions || result?.ChapterMarkerInstructions || [];
|
||||
const sceneInstructions = result?.sceneMarkerInstructions || result?.SceneMarkerInstructions || [];
|
||||
if (!lastOnboardingScanPreview && chapterInstructions.length === 0 && sceneInstructions.length === 0) {
|
||||
throw new Error("Word marker instructions are unavailable.");
|
||||
}
|
||||
|
||||
try {
|
||||
await window.Word.run(async (context) => {
|
||||
const paragraphs = context.document.body.paragraphs;
|
||||
paragraphs.load("items/text,items/style,items/styleBuiltIn");
|
||||
await context.sync();
|
||||
|
||||
if (chapterInstructions.length > 0 || sceneInstructions.length > 0) {
|
||||
for (const instruction of chapterInstructions) {
|
||||
const index = Number.parseInt(instruction.wordParagraphIndex ?? instruction.WordParagraphIndex, 10);
|
||||
const tag = instruction.markerTag || instruction.MarkerTag;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
for (const instruction of sceneInstructions) {
|
||||
const index = Number.parseInt(instruction.wordParagraphIndex ?? instruction.WordParagraphIndex, 10);
|
||||
const tag = instruction.markerTag || instruction.MarkerTag;
|
||||
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);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (const chapter of lastOnboardingScanPreview.chapters || []) {
|
||||
const chapterId = chapterMap.get(chapter.temporaryChapterKey);
|
||||
const index = Number.parseInt(chapter.startPosition, 10);
|
||||
@ -2420,10 +2460,26 @@
|
||||
ensureAnchorControl(paragraphs.items[index], "PlotDirector Scene", `PD-SCENE-${sceneId}`, "PD-SCENE");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 (Number.isInteger(buildId) && buildId > 0) {
|
||||
await companionPresenceConnection.invoke("CompleteOnboardingBuildMarkerSync", buildId);
|
||||
}
|
||||
} catch (error) {
|
||||
if (Number.isInteger(buildId) && buildId > 0) {
|
||||
await companionPresenceConnection.invoke("FailOnboardingBuildMarkerSync", buildId, errorText(error));
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const friendlyScanError = (error) => {
|
||||
|
||||
@ -154,6 +154,7 @@
|
||||
.withUrl("/hubs/word-companion-follow")
|
||||
.withAutomaticReconnect()
|
||||
.build();
|
||||
let pendingBuildPreviewId = "";
|
||||
|
||||
connection.on("WordCompanionPresenceChanged", applyStatus);
|
||||
connection.on("OnboardingScanStateChanged", applyScanState);
|
||||
@ -183,6 +184,11 @@
|
||||
window.location.href = `/onboarding/build-complete?previewId=${encodeURIComponent(previewId)}`;
|
||||
}
|
||||
});
|
||||
connection.on("OnboardingBuildMarkerSyncCompleted", () => {
|
||||
if (pendingBuildPreviewId) {
|
||||
window.location.href = `/onboarding/build-complete?previewId=${encodeURIComponent(pendingBuildPreviewId)}`;
|
||||
}
|
||||
});
|
||||
connection.onreconnecting(() => applyStatus({ status: "Connecting", isConnected: false }));
|
||||
connection.onreconnected(() => connection.invoke("WatchCompanionPresence").then(applyStatus).catch(() => {}));
|
||||
connection.onclose(() => applyStatus({ status: "Offline", isConnected: false }));
|
||||
@ -229,8 +235,10 @@
|
||||
});
|
||||
try {
|
||||
const result = await connection.invoke("StartOnboardingProjectBuild", previewId);
|
||||
const returnedPreviewId = field(result, "previewID", "PreviewID") || previewId;
|
||||
window.location.href = `/onboarding/build-complete?previewId=${encodeURIComponent(returnedPreviewId)}`;
|
||||
pendingBuildPreviewId = field(result, "previewID", "PreviewID") || previewId;
|
||||
document.querySelectorAll("[data-onboarding-build-message]").forEach((node) => {
|
||||
node.textContent = "Updating and saving Word markers. Keep Word and this browser page open.";
|
||||
});
|
||||
} catch (error) {
|
||||
button.disabled = false;
|
||||
document.querySelectorAll("[data-onboarding-build-panel]").forEach((node) => {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user