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);
|
||||
}
|
||||
|
||||
[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}")]
|
||||
public async Task<IActionResult> GetManuscriptForBook(int bookId)
|
||||
{
|
||||
|
||||
@ -238,6 +238,18 @@ public sealed class WordCompanionManuscriptBookResponse
|
||||
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 int BookId { get; set; }
|
||||
|
||||
@ -19,6 +19,7 @@ public interface IWordCompanionService
|
||||
Task<WordCompanionSyncManuscriptResponse?> SyncManuscriptAsync(int bookId, WordCompanionSyncManuscriptRequest request);
|
||||
Task<WordCompanionUpdateSceneLinksResponse?> UpdateSceneLinksAsync(int sceneId, WordCompanionUpdateSceneLinksRequest request);
|
||||
Task<WordCompanionUpdateWordCountResponse?> UpdateWordCountAsync(int sceneId, WordCompanionUpdateWordCountRequest request);
|
||||
Task<WordCompanionRuntimeBookResponse?> GetRuntimeBookAsync(int bookId, Guid documentGuid);
|
||||
Task<WordCompanionManuscriptBookResponse?> GetManuscriptForBookAsync(int bookId);
|
||||
Task<WordCompanionManuscriptLinkResponse?> LinkManuscriptAsync(WordCompanionManuscriptLinkRequest request);
|
||||
Task<WordCompanionManuscriptAnalyseResponse?> AnalyseManuscriptAsync(WordCompanionManuscriptAnalyseRequest request);
|
||||
@ -221,6 +222,50 @@ public sealed class WordCompanionService(
|
||||
: 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)
|
||||
{
|
||||
if (bookId <= 0)
|
||||
|
||||
@ -154,6 +154,10 @@
|
||||
<span>Revision</span>
|
||||
<strong data-plotdirector-revision-status>-</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Last Sync</span>
|
||||
<strong data-runtime-last-sync>-</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Blocked</span>
|
||||
<strong data-plotdirector-blocked-status>-</strong>
|
||||
|
||||
@ -74,6 +74,7 @@
|
||||
const plotDirectorBlockedStatus = document.querySelector("[data-plotdirector-blocked-status]");
|
||||
const plotDirectorBlockedReasonRow = document.querySelector("[data-plotdirector-blocked-reason-row]");
|
||||
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 syncWordCount = document.querySelector("[data-sync-word-count]");
|
||||
const syncPlotDirectorCount = document.querySelector("[data-sync-plotdirector-count]");
|
||||
@ -177,6 +178,10 @@
|
||||
let pendingCreateChapterConfirmation = null;
|
||||
let structurePreviewModel = null;
|
||||
let firstRunDocumentBinding = null;
|
||||
let runtimeBookContext = null;
|
||||
let documentStructureCache = null;
|
||||
let plotDirectorStructureCache = null;
|
||||
let plotDirectorStructureBookId = null;
|
||||
let firstRunAnalysis = null;
|
||||
let firstRunImportStructure = null;
|
||||
let firstRunCharacterCandidatesModel = [];
|
||||
@ -467,6 +472,33 @@
|
||||
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) => {
|
||||
setText(sceneLinksStatus, message);
|
||||
};
|
||||
@ -755,7 +787,13 @@
|
||||
};
|
||||
|
||||
const getBookStructureChapters = async (bookId) => {
|
||||
if (plotDirectorStructureBookId === bookId && Array.isArray(plotDirectorStructureCache?.chapters)) {
|
||||
return plotDirectorStructureCache.chapters;
|
||||
}
|
||||
|
||||
const structure = await fetchJson(`/api/word-companion/books/${bookId}/structure`);
|
||||
plotDirectorStructureBookId = bookId;
|
||||
plotDirectorStructureCache = structure || null;
|
||||
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) => {
|
||||
if (!documentOutline) {
|
||||
return;
|
||||
@ -1828,6 +2051,35 @@
|
||||
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 () => {
|
||||
setDocumentMessage("");
|
||||
|
||||
@ -1846,9 +2098,16 @@
|
||||
|
||||
try {
|
||||
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);
|
||||
});
|
||||
|
||||
if (!documentStructureCache) {
|
||||
setCurrentStructure(
|
||||
structure.currentChapter?.title,
|
||||
structure.currentScene?.title,
|
||||
@ -1857,6 +2116,7 @@
|
||||
structure.currentScene?.anchorId,
|
||||
structure.currentChapter?.index
|
||||
);
|
||||
}
|
||||
resetPlotDirectorScene("Not detected");
|
||||
renderOutline(structure);
|
||||
setDocumentMessage("");
|
||||
@ -2028,6 +2288,7 @@
|
||||
throw new Error("Chapter creation did not return a chapter ID.");
|
||||
}
|
||||
|
||||
clearPlotDirectorStructureCache();
|
||||
item.pdChapter = {
|
||||
chapterId: response.chapterId,
|
||||
title: response.title || item.wordTitle,
|
||||
@ -2052,6 +2313,7 @@
|
||||
throw new Error("Scene creation did not return a scene ID.");
|
||||
}
|
||||
|
||||
clearPlotDirectorStructureCache();
|
||||
item.pdChapter = chapter;
|
||||
item.pdScene = {
|
||||
sceneId: response.sceneId,
|
||||
@ -2188,6 +2450,7 @@
|
||||
isApplyingStructureSync = false;
|
||||
setAnalyseButtonEnabled();
|
||||
if (finalStatus) {
|
||||
clearPlotDirectorStructureCache();
|
||||
await analyseManuscriptStructure();
|
||||
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 () => {
|
||||
if (!isAuthenticated || !wordHostAvailable) {
|
||||
setFirstRunMode(false);
|
||||
@ -2489,18 +2791,24 @@
|
||||
const binding = readDocumentBinding();
|
||||
if (binding) {
|
||||
try {
|
||||
const response = await fetchJson(`/api/word-companion/manuscript/book/${binding.bookId}`);
|
||||
const document = response?.manuscriptDocument;
|
||||
if (document && String(document.documentGuid).toLowerCase() === binding.documentGuid.toLowerCase()) {
|
||||
const runtime = await fetchJson(`/api/word-companion/runtime/book/${binding.bookId}?documentGuid=${encodeURIComponent(binding.documentGuid)}`);
|
||||
const document = runtime?.manuscriptDocument;
|
||||
if (runtime?.bookId === binding.bookId
|
||||
&& runtime?.projectId === binding.projectId
|
||||
&& document
|
||||
&& String(document.documentGuid).toLowerCase() === binding.documentGuid.toLowerCase()) {
|
||||
firstRunDocumentBinding = binding;
|
||||
writeStoredId(storageKeys.projectId, binding.projectId);
|
||||
writeStoredId(storageKeys.bookId, binding.bookId);
|
||||
setRuntimeBookContext(runtime);
|
||||
writeStoredId(storageKeys.projectId, runtime.projectId);
|
||||
writeStoredId(storageKeys.bookId, runtime.bookId);
|
||||
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);
|
||||
setConnectionStatus("Connected");
|
||||
setBookMessage("Bound manuscript detected.");
|
||||
await loadRuntimeDocumentAwareness();
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
@ -2509,6 +2817,7 @@
|
||||
}
|
||||
|
||||
firstRunDocumentBinding = null;
|
||||
clearRuntimeCaches();
|
||||
syncFirstRunProjectOptions();
|
||||
if (selectedFirstRunProject()) {
|
||||
await loadBooks(selectedFirstRunProject().projectId, null);
|
||||
@ -2833,6 +3142,7 @@
|
||||
throw new Error("Chapter creation did not return a chapter ID.");
|
||||
}
|
||||
|
||||
clearPlotDirectorStructureCache();
|
||||
setResolvedScene(null, response.chapterId, "Manual chapter link");
|
||||
const anchored = await attachResolvedChapterId("Chapter created and linked successfully.");
|
||||
if (!anchored) {
|
||||
@ -3061,6 +3371,7 @@
|
||||
throw new Error("Scene creation did not return a scene ID.");
|
||||
}
|
||||
|
||||
clearPlotDirectorStructureCache();
|
||||
await resolveCreatedOrLinkedScene(response.sceneId, response.chapterId, "Scene created and linked successfully.");
|
||||
} catch (error) {
|
||||
setCreateLinkStatus("Unable to create scene.");
|
||||
@ -3535,36 +3846,23 @@
|
||||
return;
|
||||
}
|
||||
|
||||
if (!documentStructureCache) {
|
||||
setSceneTrackingState("Manual");
|
||||
setDocumentMessage("Document structure is not cached yet. Use Refresh Current Scene.");
|
||||
return;
|
||||
}
|
||||
|
||||
isAutoRefreshingScene = true;
|
||||
setSceneTrackingState("Refreshing");
|
||||
setDocumentMessage("Updating current scene...");
|
||||
setSceneTrackingState("Live");
|
||||
|
||||
try {
|
||||
const structure = await window.Word.run(async (context) => {
|
||||
return await readDocumentStructure(context);
|
||||
});
|
||||
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)
|
||||
});
|
||||
const selectionParagraphs = await window.Word.run(async (context) => await readSelectionParagraphs(context));
|
||||
const selectedIndex = findSelectedParagraphIndex(documentStructureCache, selectionParagraphs);
|
||||
const changed = applyRuntimeSelection(selectedIndex);
|
||||
|
||||
if (!changed) {
|
||||
setSceneContextStale(false);
|
||||
setSceneTrackingState("Live");
|
||||
setDocumentMessage("Current scene updated.");
|
||||
return;
|
||||
}
|
||||
|
||||
@ -3575,10 +3873,10 @@
|
||||
}
|
||||
setSceneContextStale(false);
|
||||
setSceneTrackingState("Live");
|
||||
setDocumentMessage("Current scene updated.");
|
||||
setDocumentMessage("");
|
||||
} catch (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.");
|
||||
} finally {
|
||||
isAutoRefreshingScene = false;
|
||||
@ -3677,6 +3975,8 @@
|
||||
};
|
||||
|
||||
const loadBooks = async (projectId, preferredBookId) => {
|
||||
documentStructureCache = null;
|
||||
clearPlotDirectorStructureCache();
|
||||
if (!projectId) {
|
||||
books = [];
|
||||
resetSelect(bookSelect, "Select a project first");
|
||||
@ -3804,6 +4104,7 @@
|
||||
|
||||
projectSelect?.addEventListener("change", async () => {
|
||||
const projectId = Number.parseInt(projectSelect.value || "", 10);
|
||||
clearRuntimeCaches();
|
||||
if (firstRunProjectSelect && Number.isInteger(projectId) && projectId > 0) {
|
||||
firstRunProjectSelect.value = String(projectId);
|
||||
}
|
||||
@ -3817,6 +4118,7 @@
|
||||
|
||||
bookSelect?.addEventListener("change", () => {
|
||||
const bookId = Number.parseInt(bookSelect.value || "", 10);
|
||||
clearRuntimeCaches();
|
||||
writeStoredId(storageKeys.bookId, Number.isInteger(bookId) && bookId > 0 ? bookId : null);
|
||||
if (firstRunBookSelect && Number.isInteger(bookId) && bookId > 0) {
|
||||
firstRunBookSelect.value = String(bookId);
|
||||
@ -3833,6 +4135,7 @@
|
||||
|
||||
firstRunProjectSelect?.addEventListener("change", async () => {
|
||||
const projectId = Number.parseInt(firstRunProjectSelect.value || "", 10);
|
||||
clearRuntimeCaches();
|
||||
if (projectSelect && Number.isInteger(projectId) && projectId > 0) {
|
||||
projectSelect.value = String(projectId);
|
||||
}
|
||||
@ -3844,6 +4147,7 @@
|
||||
|
||||
firstRunBookSelect?.addEventListener("change", () => {
|
||||
const bookId = Number.parseInt(firstRunBookSelect.value || "", 10);
|
||||
clearRuntimeCaches();
|
||||
if (bookSelect && Number.isInteger(bookId) && bookId > 0) {
|
||||
bookSelect.value = String(bookId);
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
Loading…
x
Reference in New Issue
Block a user