Phase 16E - Bound Document Runtime and Current Scene Detection
This commit is contained in:
parent
772a144d70
commit
f26cd48173
@ -102,6 +102,13 @@ public sealed class WordCompanionController(IWordCompanionService wordCompanion)
|
|||||||
return response is null ? BadRequest() : Ok(response);
|
return response is null ? BadRequest() : Ok(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet("runtime/book/{bookId:int}")]
|
||||||
|
public async Task<IActionResult> GetRuntimeBook(int bookId, [FromQuery] Guid documentGuid)
|
||||||
|
{
|
||||||
|
var response = await wordCompanion.GetRuntimeBookAsync(bookId, documentGuid);
|
||||||
|
return response is null ? NotFound() : Ok(response);
|
||||||
|
}
|
||||||
|
|
||||||
[HttpGet("manuscript/book/{bookId:int}")]
|
[HttpGet("manuscript/book/{bookId:int}")]
|
||||||
public async Task<IActionResult> GetManuscriptForBook(int bookId)
|
public async Task<IActionResult> GetManuscriptForBook(int bookId)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -238,6 +238,18 @@ public sealed class WordCompanionManuscriptBookResponse
|
|||||||
public WordCompanionManuscriptDocumentDto? ManuscriptDocument { get; init; }
|
public WordCompanionManuscriptDocumentDto? ManuscriptDocument { get; init; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public sealed class WordCompanionRuntimeBookResponse
|
||||||
|
{
|
||||||
|
public int ProjectId { get; init; }
|
||||||
|
public string ProjectTitle { get; init; } = string.Empty;
|
||||||
|
public int BookId { get; init; }
|
||||||
|
public string BookTitle { get; init; } = string.Empty;
|
||||||
|
public string? BookSubtitle { get; init; }
|
||||||
|
public bool UsesExplicitScenes { get; init; }
|
||||||
|
public DateTime? LastSyncUtc { get; init; }
|
||||||
|
public WordCompanionManuscriptDocumentDto ManuscriptDocument { get; init; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
public sealed class WordCompanionManuscriptLinkRequest
|
public sealed class WordCompanionManuscriptLinkRequest
|
||||||
{
|
{
|
||||||
public int BookId { get; set; }
|
public int BookId { get; set; }
|
||||||
|
|||||||
@ -19,6 +19,7 @@ public interface IWordCompanionService
|
|||||||
Task<WordCompanionSyncManuscriptResponse?> SyncManuscriptAsync(int bookId, WordCompanionSyncManuscriptRequest request);
|
Task<WordCompanionSyncManuscriptResponse?> SyncManuscriptAsync(int bookId, WordCompanionSyncManuscriptRequest request);
|
||||||
Task<WordCompanionUpdateSceneLinksResponse?> UpdateSceneLinksAsync(int sceneId, WordCompanionUpdateSceneLinksRequest request);
|
Task<WordCompanionUpdateSceneLinksResponse?> UpdateSceneLinksAsync(int sceneId, WordCompanionUpdateSceneLinksRequest request);
|
||||||
Task<WordCompanionUpdateWordCountResponse?> UpdateWordCountAsync(int sceneId, WordCompanionUpdateWordCountRequest request);
|
Task<WordCompanionUpdateWordCountResponse?> UpdateWordCountAsync(int sceneId, WordCompanionUpdateWordCountRequest request);
|
||||||
|
Task<WordCompanionRuntimeBookResponse?> GetRuntimeBookAsync(int bookId, Guid documentGuid);
|
||||||
Task<WordCompanionManuscriptBookResponse?> GetManuscriptForBookAsync(int bookId);
|
Task<WordCompanionManuscriptBookResponse?> GetManuscriptForBookAsync(int bookId);
|
||||||
Task<WordCompanionManuscriptLinkResponse?> LinkManuscriptAsync(WordCompanionManuscriptLinkRequest request);
|
Task<WordCompanionManuscriptLinkResponse?> LinkManuscriptAsync(WordCompanionManuscriptLinkRequest request);
|
||||||
Task<WordCompanionManuscriptAnalyseResponse?> AnalyseManuscriptAsync(WordCompanionManuscriptAnalyseRequest request);
|
Task<WordCompanionManuscriptAnalyseResponse?> AnalyseManuscriptAsync(WordCompanionManuscriptAnalyseRequest request);
|
||||||
@ -221,6 +222,50 @@ public sealed class WordCompanionService(
|
|||||||
: repository.UpdateWordCountAsync(sceneId, RequireUserId(), actualWords.Value, request.LastWorkedOn);
|
: repository.UpdateWordCountAsync(sceneId, RequireUserId(), actualWords.Value, request.LastWorkedOn);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<WordCompanionRuntimeBookResponse?> GetRuntimeBookAsync(int bookId, Guid documentGuid)
|
||||||
|
{
|
||||||
|
if (bookId <= 0 || documentGuid == Guid.Empty)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var userId = RequireUserId();
|
||||||
|
var document = await manuscriptDocuments.GetByGuidAsync(documentGuid, userId);
|
||||||
|
if (document is null || document.BookID != bookId || !document.IsActive)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
await manuscriptDocuments.UpdateLastOpenedAsync(document.ManuscriptDocumentID, userId);
|
||||||
|
document = await manuscriptDocuments.GetByGuidAsync(documentGuid, userId) ?? document;
|
||||||
|
|
||||||
|
var projects = await repository.ListProjectsAsync(userId);
|
||||||
|
var project = projects.FirstOrDefault(item => item.ProjectId == document.ProjectID);
|
||||||
|
if (project is null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var books = await repository.ListBooksAsync(document.ProjectID, userId);
|
||||||
|
var book = books.FirstOrDefault(item => item.BookId == document.BookID);
|
||||||
|
if (book is null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new WordCompanionRuntimeBookResponse
|
||||||
|
{
|
||||||
|
ProjectId = project.ProjectId,
|
||||||
|
ProjectTitle = project.Title,
|
||||||
|
BookId = book.BookId,
|
||||||
|
BookTitle = book.Title,
|
||||||
|
BookSubtitle = book.Subtitle,
|
||||||
|
UsesExplicitScenes = book.UsesExplicitScenes,
|
||||||
|
LastSyncUtc = document.LastSyncUtc,
|
||||||
|
ManuscriptDocument = ToDto(document)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<WordCompanionManuscriptBookResponse?> GetManuscriptForBookAsync(int bookId)
|
public async Task<WordCompanionManuscriptBookResponse?> GetManuscriptForBookAsync(int bookId)
|
||||||
{
|
{
|
||||||
if (bookId <= 0)
|
if (bookId <= 0)
|
||||||
|
|||||||
@ -154,6 +154,10 @@
|
|||||||
<span>Revision</span>
|
<span>Revision</span>
|
||||||
<strong data-plotdirector-revision-status>-</strong>
|
<strong data-plotdirector-revision-status>-</strong>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>Last Sync</span>
|
||||||
|
<strong data-runtime-last-sync>-</strong>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span>Blocked</span>
|
<span>Blocked</span>
|
||||||
<strong data-plotdirector-blocked-status>-</strong>
|
<strong data-plotdirector-blocked-status>-</strong>
|
||||||
|
|||||||
@ -74,6 +74,7 @@
|
|||||||
const plotDirectorBlockedStatus = document.querySelector("[data-plotdirector-blocked-status]");
|
const plotDirectorBlockedStatus = document.querySelector("[data-plotdirector-blocked-status]");
|
||||||
const plotDirectorBlockedReasonRow = document.querySelector("[data-plotdirector-blocked-reason-row]");
|
const plotDirectorBlockedReasonRow = document.querySelector("[data-plotdirector-blocked-reason-row]");
|
||||||
const plotDirectorBlockedReason = document.querySelector("[data-plotdirector-blocked-reason]");
|
const plotDirectorBlockedReason = document.querySelector("[data-plotdirector-blocked-reason]");
|
||||||
|
const runtimeLastSync = document.querySelector("[data-runtime-last-sync]");
|
||||||
const syncSceneProgressButton = document.querySelector("[data-sync-scene-progress]");
|
const syncSceneProgressButton = document.querySelector("[data-sync-scene-progress]");
|
||||||
const syncWordCount = document.querySelector("[data-sync-word-count]");
|
const syncWordCount = document.querySelector("[data-sync-word-count]");
|
||||||
const syncPlotDirectorCount = document.querySelector("[data-sync-plotdirector-count]");
|
const syncPlotDirectorCount = document.querySelector("[data-sync-plotdirector-count]");
|
||||||
@ -177,6 +178,10 @@
|
|||||||
let pendingCreateChapterConfirmation = null;
|
let pendingCreateChapterConfirmation = null;
|
||||||
let structurePreviewModel = null;
|
let structurePreviewModel = null;
|
||||||
let firstRunDocumentBinding = null;
|
let firstRunDocumentBinding = null;
|
||||||
|
let runtimeBookContext = null;
|
||||||
|
let documentStructureCache = null;
|
||||||
|
let plotDirectorStructureCache = null;
|
||||||
|
let plotDirectorStructureBookId = null;
|
||||||
let firstRunAnalysis = null;
|
let firstRunAnalysis = null;
|
||||||
let firstRunImportStructure = null;
|
let firstRunImportStructure = null;
|
||||||
let firstRunCharacterCandidatesModel = [];
|
let firstRunCharacterCandidatesModel = [];
|
||||||
@ -467,6 +472,33 @@
|
|||||||
return String(value);
|
return String(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const formatDateTime = (value) => {
|
||||||
|
if (!value) {
|
||||||
|
return "-";
|
||||||
|
}
|
||||||
|
|
||||||
|
const date = new Date(value);
|
||||||
|
return Number.isNaN(date.getTime()) ? "-" : date.toLocaleString();
|
||||||
|
};
|
||||||
|
|
||||||
|
const setRuntimeBookContext = (context) => {
|
||||||
|
runtimeBookContext = context || null;
|
||||||
|
setText(runtimeLastSync, formatDateTime(runtimeBookContext?.lastSyncUtc || runtimeBookContext?.manuscriptDocument?.lastSyncUtc));
|
||||||
|
setText(syncLastSynced, formatDateTime(runtimeBookContext?.lastSyncUtc || runtimeBookContext?.manuscriptDocument?.lastSyncUtc));
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearRuntimeCaches = () => {
|
||||||
|
documentStructureCache = null;
|
||||||
|
plotDirectorStructureCache = null;
|
||||||
|
plotDirectorStructureBookId = null;
|
||||||
|
setRuntimeBookContext(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearPlotDirectorStructureCache = () => {
|
||||||
|
plotDirectorStructureCache = null;
|
||||||
|
plotDirectorStructureBookId = null;
|
||||||
|
};
|
||||||
|
|
||||||
const setSceneLinksStatus = (message) => {
|
const setSceneLinksStatus = (message) => {
|
||||||
setText(sceneLinksStatus, message);
|
setText(sceneLinksStatus, message);
|
||||||
};
|
};
|
||||||
@ -755,7 +787,13 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getBookStructureChapters = async (bookId) => {
|
const getBookStructureChapters = async (bookId) => {
|
||||||
|
if (plotDirectorStructureBookId === bookId && Array.isArray(plotDirectorStructureCache?.chapters)) {
|
||||||
|
return plotDirectorStructureCache.chapters;
|
||||||
|
}
|
||||||
|
|
||||||
const structure = await fetchJson(`/api/word-companion/books/${bookId}/structure`);
|
const structure = await fetchJson(`/api/word-companion/books/${bookId}/structure`);
|
||||||
|
plotDirectorStructureBookId = bookId;
|
||||||
|
plotDirectorStructureCache = structure || null;
|
||||||
return Array.isArray(structure?.chapters) ? structure.chapters : [];
|
return Array.isArray(structure?.chapters) ? structure.chapters : [];
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -1405,6 +1443,191 @@
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const sceneSeparatorTexts = new Set(["***", "###"]);
|
||||||
|
|
||||||
|
const snapshotParagraph = (paragraph, index) => {
|
||||||
|
const text = String(paragraph?.text || "").trim();
|
||||||
|
return {
|
||||||
|
index,
|
||||||
|
text,
|
||||||
|
styleBuiltIn: paragraph?.styleBuiltIn || "",
|
||||||
|
style: paragraph?.style || "",
|
||||||
|
wordCount: countWords(text),
|
||||||
|
chapterAnchorId: paragraphAnchorId(paragraph, "PD-CHAPTER"),
|
||||||
|
sceneAnchorId: paragraphAnchorId(paragraph, "PD-SCENE")
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const finishRuntimeScene = (scene, endIndex) => {
|
||||||
|
if (scene) {
|
||||||
|
scene.endParagraphIndex = endIndex;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const finishRuntimeChapter = (chapter, endIndex) => {
|
||||||
|
if (chapter) {
|
||||||
|
chapter.endParagraphIndex = endIndex;
|
||||||
|
finishRuntimeScene(chapter.scenes.at(-1), endIndex);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildRuntimeDocumentStructure = (paragraphs, usesExplicitScenes) => {
|
||||||
|
const paragraphSnapshots = paragraphs.map(snapshotParagraph);
|
||||||
|
const chapters = [];
|
||||||
|
let heading1Count = 0;
|
||||||
|
let sceneBoundaryCount = 0;
|
||||||
|
let chapter = null;
|
||||||
|
let scene = null;
|
||||||
|
let documentWordCount = 0;
|
||||||
|
|
||||||
|
const startScene = (startParagraph, title = null, anchorId = null) => {
|
||||||
|
if (!chapter) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
finishRuntimeScene(scene, startParagraph.index - 1);
|
||||||
|
scene = {
|
||||||
|
index: chapter.scenes.length + 1,
|
||||||
|
paragraphIndex: startParagraph.index,
|
||||||
|
startParagraphIndex: startParagraph.index,
|
||||||
|
endParagraphIndex: startParagraph.index,
|
||||||
|
title: title || `Scene ${chapter.scenes.length + 1}`,
|
||||||
|
anchorId,
|
||||||
|
wordCount: 0
|
||||||
|
};
|
||||||
|
chapter.scenes.push(scene);
|
||||||
|
return scene;
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const paragraph of paragraphSnapshots) {
|
||||||
|
if (!paragraph.text) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isBuiltInHeading(paragraph, 1)) {
|
||||||
|
finishRuntimeChapter(chapter, paragraph.index - 1);
|
||||||
|
heading1Count += 1;
|
||||||
|
chapter = {
|
||||||
|
index: chapters.length + 1,
|
||||||
|
paragraphIndex: paragraph.index,
|
||||||
|
startParagraphIndex: paragraph.index,
|
||||||
|
endParagraphIndex: paragraph.index,
|
||||||
|
title: paragraph.text,
|
||||||
|
anchorId: paragraph.chapterAnchorId,
|
||||||
|
wordCount: 0,
|
||||||
|
scenes: []
|
||||||
|
};
|
||||||
|
chapters.push(chapter);
|
||||||
|
scene = null;
|
||||||
|
startScene(paragraph, usesExplicitScenes ? "Scene 1" : "Scene 1", paragraph.sceneAnchorId);
|
||||||
|
documentWordCount += paragraph.wordCount;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!chapter) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (usesExplicitScenes && sceneSeparatorTexts.has(paragraph.text)) {
|
||||||
|
sceneBoundaryCount += 1;
|
||||||
|
startScene(paragraph);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
chapter.wordCount += paragraph.wordCount;
|
||||||
|
if (scene) {
|
||||||
|
scene.wordCount += paragraph.wordCount;
|
||||||
|
}
|
||||||
|
documentWordCount += paragraph.wordCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
finishRuntimeChapter(chapter, paragraphSnapshots.length - 1);
|
||||||
|
|
||||||
|
return {
|
||||||
|
paragraphs: paragraphSnapshots,
|
||||||
|
chapters,
|
||||||
|
usesExplicitScenes: !!usesExplicitScenes,
|
||||||
|
paragraphCount: paragraphSnapshots.length,
|
||||||
|
heading1Count,
|
||||||
|
heading2Count: sceneBoundaryCount,
|
||||||
|
documentWordCount,
|
||||||
|
currentParagraphIndex: null,
|
||||||
|
currentChapter: null,
|
||||||
|
currentScene: null
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const paragraphMatchesSnapshot = (snapshot, paragraph) => {
|
||||||
|
return String(snapshot?.text || "").trim() === String(paragraph?.text || "").trim()
|
||||||
|
&& normalizedStyleName(snapshot) === normalizedStyleName(paragraph);
|
||||||
|
};
|
||||||
|
|
||||||
|
const findSelectedParagraphIndex = (cache, selectionParagraphs) => {
|
||||||
|
const selectedParagraph = (selectionParagraphs || []).find((paragraph) => String(paragraph.text || "").trim());
|
||||||
|
if (!cache || !selectedParagraph) {
|
||||||
|
return cache?.currentParagraphIndex ?? -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const matches = cache.paragraphs
|
||||||
|
.filter((paragraph) => paragraphMatchesSnapshot(paragraph, selectedParagraph))
|
||||||
|
.map((paragraph) => paragraph.index);
|
||||||
|
if (matches.length === 0) {
|
||||||
|
return cache.currentParagraphIndex ?? -1;
|
||||||
|
}
|
||||||
|
if (!Number.isInteger(cache.currentParagraphIndex)) {
|
||||||
|
return matches[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
return matches.reduce((best, candidate) =>
|
||||||
|
Math.abs(candidate - cache.currentParagraphIndex) < Math.abs(best - cache.currentParagraphIndex) ? candidate : best, matches[0]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const chapterForParagraphIndex = (cache, paragraphIndex) => {
|
||||||
|
if (!cache || !Number.isInteger(paragraphIndex) || paragraphIndex < 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return cache.chapters
|
||||||
|
.filter((chapter) => chapter.startParagraphIndex <= paragraphIndex)
|
||||||
|
.at(-1) || null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const sceneForParagraphIndex = (chapter, paragraphIndex) => {
|
||||||
|
if (!chapter || !Number.isInteger(paragraphIndex) || paragraphIndex < 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return chapter.scenes
|
||||||
|
.filter((item) => item.startParagraphIndex <= paragraphIndex)
|
||||||
|
.at(-1) || null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const applyRuntimeSelection = (paragraphIndex) => {
|
||||||
|
if (!documentStructureCache) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentChapterItem = chapterForParagraphIndex(documentStructureCache, paragraphIndex);
|
||||||
|
const currentSceneItem = sceneForParagraphIndex(currentChapterItem, paragraphIndex);
|
||||||
|
const changed = currentChapterItem?.paragraphIndex !== documentStructureCache.currentChapter?.paragraphIndex
|
||||||
|
|| currentSceneItem?.paragraphIndex !== documentStructureCache.currentScene?.paragraphIndex;
|
||||||
|
|
||||||
|
documentStructureCache.currentParagraphIndex = paragraphIndex;
|
||||||
|
documentStructureCache.currentChapter = currentChapterItem;
|
||||||
|
documentStructureCache.currentScene = currentSceneItem;
|
||||||
|
|
||||||
|
setCurrentStructure(
|
||||||
|
currentChapterItem?.title,
|
||||||
|
currentSceneItem?.title,
|
||||||
|
currentSceneItem?.wordCount,
|
||||||
|
currentChapterItem?.anchorId,
|
||||||
|
currentSceneItem?.anchorId,
|
||||||
|
currentChapterItem?.index
|
||||||
|
);
|
||||||
|
|
||||||
|
return changed;
|
||||||
|
};
|
||||||
|
|
||||||
const renderOutline = (structure) => {
|
const renderOutline = (structure) => {
|
||||||
if (!documentOutline) {
|
if (!documentOutline) {
|
||||||
return;
|
return;
|
||||||
@ -1828,6 +2051,35 @@
|
|||||||
return structure;
|
return structure;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const readRuntimeDocumentStructure = async (context, usesExplicitScenes) => {
|
||||||
|
const bodyParagraphs = context.document.body.paragraphs;
|
||||||
|
const selectionParagraphs = context.document.getSelection().paragraphs;
|
||||||
|
|
||||||
|
bodyParagraphs.load("text,styleBuiltIn,style");
|
||||||
|
selectionParagraphs.load("text,styleBuiltIn,style");
|
||||||
|
await context.sync();
|
||||||
|
|
||||||
|
for (const paragraph of bodyParagraphs.items) {
|
||||||
|
if (isBuiltInHeading(paragraph, 1)) {
|
||||||
|
paragraph.contentControls.load("items/tag,title");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await context.sync();
|
||||||
|
|
||||||
|
const cache = buildRuntimeDocumentStructure(bodyParagraphs.items, usesExplicitScenes);
|
||||||
|
const selectedIndex = findSelectedParagraphIndex(cache, selectionParagraphs.items);
|
||||||
|
documentStructureCache = cache;
|
||||||
|
applyRuntimeSelection(selectedIndex);
|
||||||
|
return cache;
|
||||||
|
};
|
||||||
|
|
||||||
|
const readSelectionParagraphs = async (context) => {
|
||||||
|
const selectionParagraphs = context.document.getSelection().paragraphs;
|
||||||
|
selectionParagraphs.load("text,styleBuiltIn,style");
|
||||||
|
await context.sync();
|
||||||
|
return selectionParagraphs.items;
|
||||||
|
};
|
||||||
|
|
||||||
const refreshDocumentStructure = async () => {
|
const refreshDocumentStructure = async () => {
|
||||||
setDocumentMessage("");
|
setDocumentMessage("");
|
||||||
|
|
||||||
@ -1846,17 +2098,25 @@
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const structure = await window.Word.run(async (context) => {
|
const structure = await window.Word.run(async (context) => {
|
||||||
|
const runtimeBook = selectedBook();
|
||||||
|
if (runtimeBook) {
|
||||||
|
return await readRuntimeDocumentStructure(context, !!runtimeBook.usesExplicitScenes);
|
||||||
|
}
|
||||||
|
|
||||||
|
documentStructureCache = null;
|
||||||
return await readDocumentStructure(context);
|
return await readDocumentStructure(context);
|
||||||
});
|
});
|
||||||
|
|
||||||
setCurrentStructure(
|
if (!documentStructureCache) {
|
||||||
structure.currentChapter?.title,
|
setCurrentStructure(
|
||||||
structure.currentScene?.title,
|
structure.currentChapter?.title,
|
||||||
structure.currentScene?.wordCount,
|
structure.currentScene?.title,
|
||||||
structure.currentChapter?.anchorId,
|
structure.currentScene?.wordCount,
|
||||||
structure.currentScene?.anchorId,
|
structure.currentChapter?.anchorId,
|
||||||
structure.currentChapter?.index
|
structure.currentScene?.anchorId,
|
||||||
);
|
structure.currentChapter?.index
|
||||||
|
);
|
||||||
|
}
|
||||||
resetPlotDirectorScene("Not detected");
|
resetPlotDirectorScene("Not detected");
|
||||||
renderOutline(structure);
|
renderOutline(structure);
|
||||||
setDocumentMessage("");
|
setDocumentMessage("");
|
||||||
@ -2028,6 +2288,7 @@
|
|||||||
throw new Error("Chapter creation did not return a chapter ID.");
|
throw new Error("Chapter creation did not return a chapter ID.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clearPlotDirectorStructureCache();
|
||||||
item.pdChapter = {
|
item.pdChapter = {
|
||||||
chapterId: response.chapterId,
|
chapterId: response.chapterId,
|
||||||
title: response.title || item.wordTitle,
|
title: response.title || item.wordTitle,
|
||||||
@ -2052,6 +2313,7 @@
|
|||||||
throw new Error("Scene creation did not return a scene ID.");
|
throw new Error("Scene creation did not return a scene ID.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clearPlotDirectorStructureCache();
|
||||||
item.pdChapter = chapter;
|
item.pdChapter = chapter;
|
||||||
item.pdScene = {
|
item.pdScene = {
|
||||||
sceneId: response.sceneId,
|
sceneId: response.sceneId,
|
||||||
@ -2188,6 +2450,7 @@
|
|||||||
isApplyingStructureSync = false;
|
isApplyingStructureSync = false;
|
||||||
setAnalyseButtonEnabled();
|
setAnalyseButtonEnabled();
|
||||||
if (finalStatus) {
|
if (finalStatus) {
|
||||||
|
clearPlotDirectorStructureCache();
|
||||||
await analyseManuscriptStructure();
|
await analyseManuscriptStructure();
|
||||||
setStructurePreviewStatus(`${finalStatus} Preview refreshed.`);
|
setStructurePreviewStatus(`${finalStatus} Preview refreshed.`);
|
||||||
}
|
}
|
||||||
@ -2480,6 +2743,45 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const loadRuntimeDocumentAwareness = async () => {
|
||||||
|
const book = selectedBook();
|
||||||
|
if (!book || !officeReady || !wordHostAvailable || !window.Word || typeof window.Word.run !== "function") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
setDocumentMessage("Reading manuscript position...");
|
||||||
|
try {
|
||||||
|
clearPlotDirectorStructureCache();
|
||||||
|
await getBookStructureChapters(book.bookId);
|
||||||
|
const structure = await window.Word.run(async (context) => await readRuntimeDocumentStructure(context, !!book.usesExplicitScenes));
|
||||||
|
renderOutline(structure);
|
||||||
|
setDiagnostics({
|
||||||
|
lastScan: "Success",
|
||||||
|
lastError: "-",
|
||||||
|
paragraphs: String(structure.paragraphCount),
|
||||||
|
heading1: String(structure.heading1Count),
|
||||||
|
heading2: String(structure.heading2Count)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!detectedSceneTitle) {
|
||||||
|
await refreshPlotDirectorChapter();
|
||||||
|
} else {
|
||||||
|
await refreshPlotDirectorScene();
|
||||||
|
}
|
||||||
|
setSceneContextStale(false);
|
||||||
|
setSceneTrackingState("Live");
|
||||||
|
setDocumentMessage("");
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Unable to initialise bound manuscript runtime.", error);
|
||||||
|
documentStructureCache = null;
|
||||||
|
setSceneTrackingState("Manual");
|
||||||
|
setDocumentMessage(`Unable to read manuscript position: ${errorText(error)}`);
|
||||||
|
setDiagnostics({ lastScan: "Failure", lastError: errorText(error) });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const initialiseFirstRunBinding = async () => {
|
const initialiseFirstRunBinding = async () => {
|
||||||
if (!isAuthenticated || !wordHostAvailable) {
|
if (!isAuthenticated || !wordHostAvailable) {
|
||||||
setFirstRunMode(false);
|
setFirstRunMode(false);
|
||||||
@ -2489,18 +2791,24 @@
|
|||||||
const binding = readDocumentBinding();
|
const binding = readDocumentBinding();
|
||||||
if (binding) {
|
if (binding) {
|
||||||
try {
|
try {
|
||||||
const response = await fetchJson(`/api/word-companion/manuscript/book/${binding.bookId}`);
|
const runtime = await fetchJson(`/api/word-companion/runtime/book/${binding.bookId}?documentGuid=${encodeURIComponent(binding.documentGuid)}`);
|
||||||
const document = response?.manuscriptDocument;
|
const document = runtime?.manuscriptDocument;
|
||||||
if (document && String(document.documentGuid).toLowerCase() === binding.documentGuid.toLowerCase()) {
|
if (runtime?.bookId === binding.bookId
|
||||||
|
&& runtime?.projectId === binding.projectId
|
||||||
|
&& document
|
||||||
|
&& String(document.documentGuid).toLowerCase() === binding.documentGuid.toLowerCase()) {
|
||||||
firstRunDocumentBinding = binding;
|
firstRunDocumentBinding = binding;
|
||||||
writeStoredId(storageKeys.projectId, binding.projectId);
|
setRuntimeBookContext(runtime);
|
||||||
writeStoredId(storageKeys.bookId, binding.bookId);
|
writeStoredId(storageKeys.projectId, runtime.projectId);
|
||||||
|
writeStoredId(storageKeys.bookId, runtime.bookId);
|
||||||
if (projectSelect) {
|
if (projectSelect) {
|
||||||
projectSelect.value = String(binding.projectId);
|
projectSelect.value = String(runtime.projectId);
|
||||||
}
|
}
|
||||||
await loadBooks(binding.projectId, binding.bookId);
|
await loadBooks(runtime.projectId, runtime.bookId);
|
||||||
setFirstRunMode(false);
|
setFirstRunMode(false);
|
||||||
setConnectionStatus("Connected");
|
setConnectionStatus("Connected");
|
||||||
|
setBookMessage("Bound manuscript detected.");
|
||||||
|
await loadRuntimeDocumentAwareness();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
@ -2509,6 +2817,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
firstRunDocumentBinding = null;
|
firstRunDocumentBinding = null;
|
||||||
|
clearRuntimeCaches();
|
||||||
syncFirstRunProjectOptions();
|
syncFirstRunProjectOptions();
|
||||||
if (selectedFirstRunProject()) {
|
if (selectedFirstRunProject()) {
|
||||||
await loadBooks(selectedFirstRunProject().projectId, null);
|
await loadBooks(selectedFirstRunProject().projectId, null);
|
||||||
@ -2833,6 +3142,7 @@
|
|||||||
throw new Error("Chapter creation did not return a chapter ID.");
|
throw new Error("Chapter creation did not return a chapter ID.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clearPlotDirectorStructureCache();
|
||||||
setResolvedScene(null, response.chapterId, "Manual chapter link");
|
setResolvedScene(null, response.chapterId, "Manual chapter link");
|
||||||
const anchored = await attachResolvedChapterId("Chapter created and linked successfully.");
|
const anchored = await attachResolvedChapterId("Chapter created and linked successfully.");
|
||||||
if (!anchored) {
|
if (!anchored) {
|
||||||
@ -3061,6 +3371,7 @@
|
|||||||
throw new Error("Scene creation did not return a scene ID.");
|
throw new Error("Scene creation did not return a scene ID.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clearPlotDirectorStructureCache();
|
||||||
await resolveCreatedOrLinkedScene(response.sceneId, response.chapterId, "Scene created and linked successfully.");
|
await resolveCreatedOrLinkedScene(response.sceneId, response.chapterId, "Scene created and linked successfully.");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setCreateLinkStatus("Unable to create scene.");
|
setCreateLinkStatus("Unable to create scene.");
|
||||||
@ -3535,36 +3846,23 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!documentStructureCache) {
|
||||||
|
setSceneTrackingState("Manual");
|
||||||
|
setDocumentMessage("Document structure is not cached yet. Use Refresh Current Scene.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
isAutoRefreshingScene = true;
|
isAutoRefreshingScene = true;
|
||||||
setSceneTrackingState("Refreshing");
|
setSceneTrackingState("Live");
|
||||||
setDocumentMessage("Updating current scene...");
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const structure = await window.Word.run(async (context) => {
|
const selectionParagraphs = await window.Word.run(async (context) => await readSelectionParagraphs(context));
|
||||||
return await readDocumentStructure(context);
|
const selectedIndex = findSelectedParagraphIndex(documentStructureCache, selectionParagraphs);
|
||||||
});
|
const changed = applyRuntimeSelection(selectedIndex);
|
||||||
const changed = hasCurrentWordContextChanged(structure);
|
|
||||||
setCurrentStructure(
|
|
||||||
structure.currentChapter?.title,
|
|
||||||
structure.currentScene?.title,
|
|
||||||
structure.currentScene?.wordCount,
|
|
||||||
structure.currentChapter?.anchorId,
|
|
||||||
structure.currentScene?.anchorId,
|
|
||||||
structure.currentChapter?.index
|
|
||||||
);
|
|
||||||
renderOutline(structure);
|
|
||||||
setDiagnostics({
|
|
||||||
lastScan: "Success",
|
|
||||||
lastError: "-",
|
|
||||||
paragraphs: String(structure.paragraphCount),
|
|
||||||
heading1: String(structure.heading1Count),
|
|
||||||
heading2: String(structure.heading2Count)
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!changed) {
|
if (!changed) {
|
||||||
setSceneContextStale(false);
|
setSceneContextStale(false);
|
||||||
setSceneTrackingState("Live");
|
setSceneTrackingState("Live");
|
||||||
setDocumentMessage("Current scene updated.");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -3575,10 +3873,10 @@
|
|||||||
}
|
}
|
||||||
setSceneContextStale(false);
|
setSceneContextStale(false);
|
||||||
setSceneTrackingState("Live");
|
setSceneTrackingState("Live");
|
||||||
setDocumentMessage("Current scene updated.");
|
setDocumentMessage("");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Unable to refresh current scene from Word selection.", error);
|
console.error("Unable to refresh current scene from Word selection.", error);
|
||||||
setDiagnostics({ lastScan: "Failure", lastError: errorText(error) });
|
setDiagnostics({ lastError: errorText(error) });
|
||||||
setSceneContextStale(true, "The Word cursor is now in a different scene. Refresh Current Scene before saving.");
|
setSceneContextStale(true, "The Word cursor is now in a different scene. Refresh Current Scene before saving.");
|
||||||
} finally {
|
} finally {
|
||||||
isAutoRefreshingScene = false;
|
isAutoRefreshingScene = false;
|
||||||
@ -3677,6 +3975,8 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const loadBooks = async (projectId, preferredBookId) => {
|
const loadBooks = async (projectId, preferredBookId) => {
|
||||||
|
documentStructureCache = null;
|
||||||
|
clearPlotDirectorStructureCache();
|
||||||
if (!projectId) {
|
if (!projectId) {
|
||||||
books = [];
|
books = [];
|
||||||
resetSelect(bookSelect, "Select a project first");
|
resetSelect(bookSelect, "Select a project first");
|
||||||
@ -3804,6 +4104,7 @@
|
|||||||
|
|
||||||
projectSelect?.addEventListener("change", async () => {
|
projectSelect?.addEventListener("change", async () => {
|
||||||
const projectId = Number.parseInt(projectSelect.value || "", 10);
|
const projectId = Number.parseInt(projectSelect.value || "", 10);
|
||||||
|
clearRuntimeCaches();
|
||||||
if (firstRunProjectSelect && Number.isInteger(projectId) && projectId > 0) {
|
if (firstRunProjectSelect && Number.isInteger(projectId) && projectId > 0) {
|
||||||
firstRunProjectSelect.value = String(projectId);
|
firstRunProjectSelect.value = String(projectId);
|
||||||
}
|
}
|
||||||
@ -3817,6 +4118,7 @@
|
|||||||
|
|
||||||
bookSelect?.addEventListener("change", () => {
|
bookSelect?.addEventListener("change", () => {
|
||||||
const bookId = Number.parseInt(bookSelect.value || "", 10);
|
const bookId = Number.parseInt(bookSelect.value || "", 10);
|
||||||
|
clearRuntimeCaches();
|
||||||
writeStoredId(storageKeys.bookId, Number.isInteger(bookId) && bookId > 0 ? bookId : null);
|
writeStoredId(storageKeys.bookId, Number.isInteger(bookId) && bookId > 0 ? bookId : null);
|
||||||
if (firstRunBookSelect && Number.isInteger(bookId) && bookId > 0) {
|
if (firstRunBookSelect && Number.isInteger(bookId) && bookId > 0) {
|
||||||
firstRunBookSelect.value = String(bookId);
|
firstRunBookSelect.value = String(bookId);
|
||||||
@ -3833,6 +4135,7 @@
|
|||||||
|
|
||||||
firstRunProjectSelect?.addEventListener("change", async () => {
|
firstRunProjectSelect?.addEventListener("change", async () => {
|
||||||
const projectId = Number.parseInt(firstRunProjectSelect.value || "", 10);
|
const projectId = Number.parseInt(firstRunProjectSelect.value || "", 10);
|
||||||
|
clearRuntimeCaches();
|
||||||
if (projectSelect && Number.isInteger(projectId) && projectId > 0) {
|
if (projectSelect && Number.isInteger(projectId) && projectId > 0) {
|
||||||
projectSelect.value = String(projectId);
|
projectSelect.value = String(projectId);
|
||||||
}
|
}
|
||||||
@ -3844,6 +4147,7 @@
|
|||||||
|
|
||||||
firstRunBookSelect?.addEventListener("change", () => {
|
firstRunBookSelect?.addEventListener("change", () => {
|
||||||
const bookId = Number.parseInt(firstRunBookSelect.value || "", 10);
|
const bookId = Number.parseInt(firstRunBookSelect.value || "", 10);
|
||||||
|
clearRuntimeCaches();
|
||||||
if (bookSelect && Number.isInteger(bookId) && bookId > 0) {
|
if (bookSelect && Number.isInteger(bookId) && bookId > 0) {
|
||||||
bookSelect.value = String(bookId);
|
bookSelect.value = String(bookId);
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Loading…
x
Reference in New Issue
Block a user