Phase 16P - Word Companion Performance Hardening and Office.js Snapshot Optimisation
This commit is contained in:
parent
524515e9b6
commit
db6f0992c1
@ -199,6 +199,8 @@
|
|||||||
let runtimeSelectionDirty = false;
|
let runtimeSelectionDirty = false;
|
||||||
let runtimeLastActivityAt = 0;
|
let runtimeLastActivityAt = 0;
|
||||||
let runtimeUpdatePending = false;
|
let runtimeUpdatePending = false;
|
||||||
|
let runtimeGeneration = 0;
|
||||||
|
let lastCurrentSceneSnapshot = null;
|
||||||
const runtimeIdleDelayMs = 1200;
|
const runtimeIdleDelayMs = 1200;
|
||||||
let selectionHandlerRegistered = false;
|
let selectionHandlerRegistered = false;
|
||||||
let isAutoRefreshingScene = false;
|
let isAutoRefreshingScene = false;
|
||||||
@ -244,6 +246,9 @@
|
|||||||
runtimeLastDebugAt = now;
|
runtimeLastDebugAt = now;
|
||||||
console.debug(`[Word Companion Runtime] ${message}`);
|
console.debug(`[Word Companion Runtime] ${message}`);
|
||||||
};
|
};
|
||||||
|
const runtimeTiming = (message) => {
|
||||||
|
console.debug(`[Word Companion Timing] ${message}`);
|
||||||
|
};
|
||||||
|
|
||||||
const setOfficeStatus = (message) => {
|
const setOfficeStatus = (message) => {
|
||||||
if (officeStatus) {
|
if (officeStatus) {
|
||||||
@ -586,6 +591,8 @@
|
|||||||
documentStructureCache = null;
|
documentStructureCache = null;
|
||||||
plotDirectorStructureCache = null;
|
plotDirectorStructureCache = null;
|
||||||
plotDirectorStructureBookId = null;
|
plotDirectorStructureBookId = null;
|
||||||
|
runtimeGeneration += 1;
|
||||||
|
lastCurrentSceneSnapshot = null;
|
||||||
resetRuntimeCharacterAliasCache();
|
resetRuntimeCharacterAliasCache();
|
||||||
resetRuntimeAssetAliasCache();
|
resetRuntimeAssetAliasCache();
|
||||||
resetRuntimeLocationAliasCache();
|
resetRuntimeLocationAliasCache();
|
||||||
@ -2207,73 +2214,122 @@
|
|||||||
return selectionParagraphs.items;
|
return selectionParagraphs.items;
|
||||||
};
|
};
|
||||||
|
|
||||||
const recountCachedCurrentScene = async () => {
|
const runtimePerformanceNow = () =>
|
||||||
|
window.performance && typeof window.performance.now === "function" ? window.performance.now() : Date.now();
|
||||||
|
|
||||||
|
const isSnapshotFresh = (snapshot) => !!snapshot
|
||||||
|
&& !snapshot.stale
|
||||||
|
&& snapshot.generation === runtimeGeneration
|
||||||
|
&& snapshot.documentGuid === currentDocumentGuid()
|
||||||
|
&& snapshot.sceneId === resolvedSceneId;
|
||||||
|
|
||||||
|
const captureCurrentSceneSnapshot = async (options = {}) => {
|
||||||
|
const includeSelection = !!options.includeSelection;
|
||||||
|
const updateDisplay = options.updateDisplay !== false;
|
||||||
|
const generation = runtimeGeneration;
|
||||||
|
const startedAt = runtimePerformanceNow();
|
||||||
|
const project = selectedProject();
|
||||||
|
const book = selectedBook();
|
||||||
|
|
||||||
const scene = documentStructureCache?.currentScene;
|
const scene = documentStructureCache?.currentScene;
|
||||||
if (!scene || !Number.isInteger(scene.startParagraphIndex) || !Number.isInteger(scene.endParagraphIndex)) {
|
if (!scene || !Number.isInteger(scene.startParagraphIndex) || !Number.isInteger(scene.endParagraphIndex)) {
|
||||||
return detectedSceneWordCount;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!officeReady || !wordHostAvailable || !window.Word || typeof window.Word.run !== "function") {
|
if (!officeReady || !wordHostAvailable || !window.Word || typeof window.Word.run !== "function") {
|
||||||
return detectedSceneWordCount;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return await window.Word.run(async (context) => {
|
const officeStartedAt = runtimePerformanceNow();
|
||||||
|
const wordSnapshot = await window.Word.run(async (context) => {
|
||||||
const bodyParagraphs = context.document.body.paragraphs;
|
const bodyParagraphs = context.document.body.paragraphs;
|
||||||
bodyParagraphs.load("text");
|
bodyParagraphs.load("text");
|
||||||
|
let selectionParagraphs = null;
|
||||||
|
if (includeSelection) {
|
||||||
|
selectionParagraphs = context.document.getSelection().paragraphs;
|
||||||
|
selectionParagraphs.load("text,styleBuiltIn,style");
|
||||||
|
}
|
||||||
await context.sync();
|
await context.sync();
|
||||||
|
|
||||||
let wordCount = 0;
|
return {
|
||||||
const endIndex = Math.min(scene.endParagraphIndex, bodyParagraphs.items.length - 1);
|
bodyTexts: bodyParagraphs.items.map((paragraph) => String(paragraph.text || "")),
|
||||||
const chapterHeadingIndex = documentStructureCache.currentChapter?.startParagraphIndex;
|
selectionParagraphs: includeSelection && selectionParagraphs
|
||||||
for (let index = scene.startParagraphIndex; index <= endIndex; index += 1) {
|
? selectionParagraphs.items.map((paragraph) => ({
|
||||||
const text = String(bodyParagraphs.items[index]?.text || "").trim();
|
text: String(paragraph.text || ""),
|
||||||
if (!text || sceneSeparatorTexts.has(text) || index === chapterHeadingIndex) {
|
styleBuiltIn: paragraph.styleBuiltIn,
|
||||||
continue;
|
style: paragraph.style
|
||||||
}
|
}))
|
||||||
wordCount += countWords(text);
|
: []
|
||||||
}
|
};
|
||||||
|
});
|
||||||
|
const officeMs = Math.round(runtimePerformanceNow() - officeStartedAt);
|
||||||
|
|
||||||
scene.wordCount = wordCount;
|
if (generation !== runtimeGeneration) {
|
||||||
|
runtimeTiming(`Office snapshot: ${officeMs}ms stale`);
|
||||||
|
return { stale: true, generation };
|
||||||
|
}
|
||||||
|
|
||||||
|
let selectionChanged = false;
|
||||||
|
if (includeSelection) {
|
||||||
|
const selectedIndex = findSelectedParagraphIndex(documentStructureCache, wordSnapshot.selectionParagraphs);
|
||||||
|
selectionChanged = applyRuntimeSelection(selectedIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentSceneItem = documentStructureCache?.currentScene;
|
||||||
|
if (!currentSceneItem || !Number.isInteger(currentSceneItem.startParagraphIndex) || !Number.isInteger(currentSceneItem.endParagraphIndex)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const texts = [];
|
||||||
|
let wordCount = 0;
|
||||||
|
const endIndex = Math.min(currentSceneItem.endParagraphIndex, wordSnapshot.bodyTexts.length - 1);
|
||||||
|
const chapterHeadingIndex = documentStructureCache.currentChapter?.startParagraphIndex;
|
||||||
|
for (let index = currentSceneItem.startParagraphIndex; index <= endIndex; index += 1) {
|
||||||
|
const text = String(wordSnapshot.bodyTexts[index] || "").trim();
|
||||||
|
if (!text || sceneSeparatorTexts.has(text) || index === chapterHeadingIndex) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
texts.push(text);
|
||||||
|
wordCount += countWords(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
currentSceneItem.wordCount = wordCount;
|
||||||
|
if (updateDisplay) {
|
||||||
setCurrentStructure(
|
setCurrentStructure(
|
||||||
documentStructureCache.currentChapter?.title,
|
documentStructureCache.currentChapter?.title,
|
||||||
scene.title,
|
currentSceneItem.title,
|
||||||
wordCount,
|
wordCount,
|
||||||
documentStructureCache.currentChapter?.anchorId,
|
documentStructureCache.currentChapter?.anchorId,
|
||||||
scene.anchorId,
|
currentSceneItem.anchorId,
|
||||||
documentStructureCache.currentChapter?.index
|
documentStructureCache.currentChapter?.index
|
||||||
);
|
);
|
||||||
return wordCount;
|
}
|
||||||
});
|
|
||||||
|
const snapshot = {
|
||||||
|
documentGuid: currentDocumentGuid(),
|
||||||
|
projectId: project?.projectId || null,
|
||||||
|
bookId: book?.bookId || null,
|
||||||
|
chapterId: resolvedChapterId,
|
||||||
|
sceneId: resolvedSceneId,
|
||||||
|
chapterTitle: documentStructureCache.currentChapter?.title || "",
|
||||||
|
sceneTitle: currentSceneItem.title || "",
|
||||||
|
sceneText: texts.join("\n"),
|
||||||
|
wordCount,
|
||||||
|
selectionSignature: `${documentStructureCache.currentParagraphIndex ?? -1}:${currentSceneItem.startParagraphIndex}:${currentSceneItem.endParagraphIndex}`,
|
||||||
|
selectionChanged,
|
||||||
|
generation
|
||||||
|
};
|
||||||
|
lastCurrentSceneSnapshot = snapshot;
|
||||||
|
runtimeTiming(`Office snapshot: ${officeMs}ms; scene words: ${wordCount}; total snapshot: ${Math.round(runtimePerformanceNow() - startedAt)}ms`);
|
||||||
|
return snapshot;
|
||||||
};
|
};
|
||||||
|
|
||||||
const readCachedCurrentSceneText = async () => {
|
const getFreshCurrentSceneSnapshot = async () => {
|
||||||
const scene = documentStructureCache?.currentScene;
|
if (isSnapshotFresh(lastCurrentSceneSnapshot)) {
|
||||||
if (!scene || !Number.isInteger(scene.startParagraphIndex) || !Number.isInteger(scene.endParagraphIndex)) {
|
return lastCurrentSceneSnapshot;
|
||||||
return "";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!officeReady || !wordHostAvailable || !window.Word || typeof window.Word.run !== "function") {
|
return await captureCurrentSceneSnapshot();
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
return await window.Word.run(async (context) => {
|
|
||||||
const bodyParagraphs = context.document.body.paragraphs;
|
|
||||||
bodyParagraphs.load("text");
|
|
||||||
await context.sync();
|
|
||||||
|
|
||||||
const texts = [];
|
|
||||||
const endIndex = Math.min(scene.endParagraphIndex, bodyParagraphs.items.length - 1);
|
|
||||||
const chapterHeadingIndex = documentStructureCache.currentChapter?.startParagraphIndex;
|
|
||||||
for (let index = scene.startParagraphIndex; index <= endIndex; index += 1) {
|
|
||||||
const text = String(bodyParagraphs.items[index]?.text || "").trim();
|
|
||||||
if (!text || sceneSeparatorTexts.has(text) || index === chapterHeadingIndex) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
texts.push(text);
|
|
||||||
}
|
|
||||||
|
|
||||||
return texts.join("\n");
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const refreshDocumentStructure = async () => {
|
const refreshDocumentStructure = async () => {
|
||||||
@ -3788,6 +3844,7 @@
|
|||||||
|
|
||||||
const syncSceneProgress = async (options = {}) => {
|
const syncSceneProgress = async (options = {}) => {
|
||||||
const source = options.source || "manual";
|
const source = options.source || "manual";
|
||||||
|
const totalStartedAt = runtimePerformanceNow();
|
||||||
if (!resolvedSceneId) {
|
if (!resolvedSceneId) {
|
||||||
setSyncStatus("No resolved PlotDirector scene.");
|
setSyncStatus("No resolved PlotDirector scene.");
|
||||||
return;
|
return;
|
||||||
@ -3820,9 +3877,9 @@
|
|||||||
sceneWordCountSyncInFlight = true;
|
sceneWordCountSyncInFlight = true;
|
||||||
sceneWordCountSyncPending = false;
|
sceneWordCountSyncPending = false;
|
||||||
|
|
||||||
let wordCount = detectedSceneWordCount;
|
let snapshot = options.snapshot || null;
|
||||||
try {
|
try {
|
||||||
wordCount = await recountCachedCurrentScene();
|
snapshot = isSnapshotFresh(snapshot) ? snapshot : await getFreshCurrentSceneSnapshot();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setSyncStatus("Sync failed");
|
setSyncStatus("Sync failed");
|
||||||
setDiagnostics({ lastError: errorText(error) });
|
setDiagnostics({ lastError: errorText(error) });
|
||||||
@ -3831,6 +3888,14 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!isSnapshotFresh(snapshot)) {
|
||||||
|
setSyncStatus("Waiting for typing to pause");
|
||||||
|
runtimeDebug("Scene sync skipped (stale snapshot).");
|
||||||
|
finishSceneWordCountSync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const wordCount = snapshot.wordCount;
|
||||||
if (!Number.isInteger(wordCount) || wordCount < 0) {
|
if (!Number.isInteger(wordCount) || wordCount < 0) {
|
||||||
setSyncStatus("Sync failed");
|
setSyncStatus("Sync failed");
|
||||||
runtimeDebug("Scene sync failed.");
|
runtimeDebug("Scene sync failed.");
|
||||||
@ -3838,7 +3903,7 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lastSyncedSceneId === resolvedSceneId && lastSyncedWordCount === wordCount) {
|
if (lastSyncedSceneId === snapshot.sceneId && lastSyncedWordCount === wordCount) {
|
||||||
setSyncStatus("Synced");
|
setSyncStatus("Synced");
|
||||||
runtimeDebug("Sync skipped (count unchanged).");
|
runtimeDebug("Sync skipped (count unchanged).");
|
||||||
finishSceneWordCountSync();
|
finishSceneWordCountSync();
|
||||||
@ -3855,16 +3920,22 @@
|
|||||||
setSyncStatus("Syncing...");
|
setSyncStatus("Syncing...");
|
||||||
runtimeDebug(`Scene sync started (${source}).`);
|
runtimeDebug(`Scene sync started (${source}).`);
|
||||||
try {
|
try {
|
||||||
|
const apiStartedAt = runtimePerformanceNow();
|
||||||
const response = await postJson("/api/word-companion/runtime/scene-word-count", {
|
const response = await postJson("/api/word-companion/runtime/scene-word-count", {
|
||||||
documentGuid,
|
documentGuid: snapshot.documentGuid || documentGuid,
|
||||||
bookId: book.bookId,
|
bookId: snapshot.bookId || book.bookId,
|
||||||
chapterId: resolvedChapterId,
|
chapterId: snapshot.chapterId,
|
||||||
sceneId: resolvedSceneId,
|
sceneId: snapshot.sceneId,
|
||||||
wordCount
|
wordCount
|
||||||
});
|
});
|
||||||
|
runtimeTiming(`Word count sync API: ${Math.round(runtimePerformanceNow() - apiStartedAt)}ms`);
|
||||||
|
if (!isSnapshotFresh(snapshot)) {
|
||||||
|
runtimeDebug("Scene sync result ignored (stale snapshot).");
|
||||||
|
return;
|
||||||
|
}
|
||||||
const actualWords = response?.actualWords ?? wordCount;
|
const actualWords = response?.actualWords ?? wordCount;
|
||||||
const syncedAt = new Date();
|
const syncedAt = new Date();
|
||||||
lastSyncedSceneId = resolvedSceneId;
|
lastSyncedSceneId = snapshot.sceneId;
|
||||||
lastSyncedWordCount = actualWords;
|
lastSyncedWordCount = actualWords;
|
||||||
setText(syncPlotDirectorCount, displayValue(actualWords));
|
setText(syncPlotDirectorCount, displayValue(actualWords));
|
||||||
setText(plotDirectorActualWords, displayValue(actualWords));
|
setText(plotDirectorActualWords, displayValue(actualWords));
|
||||||
@ -3872,6 +3943,7 @@
|
|||||||
setText(runtimeLastSync, formatLastSynced(syncedAt));
|
setText(runtimeLastSync, formatLastSynced(syncedAt));
|
||||||
setSyncStatus("Synced");
|
setSyncStatus("Synced");
|
||||||
runtimeDebug("Scene sync succeeded.");
|
runtimeDebug("Scene sync succeeded.");
|
||||||
|
runtimeTiming(`Total word count sync: ${Math.round(runtimePerformanceNow() - totalStartedAt)}ms`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setSyncStatus("Sync failed");
|
setSyncStatus("Sync failed");
|
||||||
setDiagnostics({ lastError: errorText(error) });
|
setDiagnostics({ lastError: errorText(error) });
|
||||||
@ -4106,34 +4178,62 @@
|
|||||||
|
|
||||||
characterSuggestionInFlight = true;
|
characterSuggestionInFlight = true;
|
||||||
characterSuggestionPending = false;
|
characterSuggestionPending = false;
|
||||||
|
const totalStartedAt = runtimePerformanceNow();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [sceneText, characterAliases, assetAliases, locationAliases] = await Promise.all([
|
const snapshot = await getFreshCurrentSceneSnapshot();
|
||||||
readCachedCurrentSceneText(),
|
if (!isSnapshotFresh(snapshot)) {
|
||||||
|
runtimeDebug("Runtime suggestions skipped (stale snapshot).");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [characterAliases, assetAliases, locationAliases] = await Promise.all([
|
||||||
loadRuntimeCharacterAliases(),
|
loadRuntimeCharacterAliases(),
|
||||||
loadRuntimeAssetAliases(),
|
loadRuntimeAssetAliases(),
|
||||||
loadRuntimeLocationAliases()
|
loadRuntimeLocationAliases()
|
||||||
]);
|
]);
|
||||||
|
if (!isSnapshotFresh(snapshot)) {
|
||||||
|
runtimeDebug("Runtime suggestions skipped after cache load (stale snapshot).");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sceneText = snapshot.sceneText || "";
|
||||||
|
const characterStartedAt = runtimePerformanceNow();
|
||||||
const detected = detectCharactersInSceneText(sceneText, characterAliases);
|
const detected = detectCharactersInSceneText(sceneText, characterAliases);
|
||||||
|
runtimeTiming(`Character detection: ${Math.round(runtimePerformanceNow() - characterStartedAt)}ms`);
|
||||||
const characterIds = detected.map((item) => item.characterId);
|
const characterIds = detected.map((item) => item.characterId);
|
||||||
const signature = characterIds.slice().sort((left, right) => left - right).join(",");
|
const signature = characterIds.slice().sort((left, right) => left - right).join(",");
|
||||||
|
const assetStartedAt = runtimePerformanceNow();
|
||||||
const detectedAssets = detectAssetsInSceneText(sceneText, assetAliases);
|
const detectedAssets = detectAssetsInSceneText(sceneText, assetAliases);
|
||||||
|
runtimeTiming(`Asset detection: ${Math.round(runtimePerformanceNow() - assetStartedAt)}ms`);
|
||||||
const assetIds = detectedAssets.map((item) => item.assetId);
|
const assetIds = detectedAssets.map((item) => item.assetId);
|
||||||
const assetSignature = assetIds.slice().sort((left, right) => left - right).join(",");
|
const assetSignature = assetIds.slice().sort((left, right) => left - right).join(",");
|
||||||
|
const locationStartedAt = runtimePerformanceNow();
|
||||||
const detectedLocations = detectLocationsInSceneText(sceneText, locationAliases);
|
const detectedLocations = detectLocationsInSceneText(sceneText, locationAliases);
|
||||||
|
runtimeTiming(`Location detection: ${Math.round(runtimePerformanceNow() - locationStartedAt)}ms`);
|
||||||
const locationIds = detectedLocations.map((item) => item.locationId);
|
const locationIds = detectedLocations.map((item) => item.locationId);
|
||||||
const locationSignature = locationIds.slice().sort((left, right) => left - right).join(",");
|
const locationSignature = locationIds.slice().sort((left, right) => left - right).join(",");
|
||||||
|
|
||||||
if (!signature || (lastSuggestedSceneId === resolvedSceneId && lastSuggestedCharacterSignature === signature)) {
|
if (!signature || (lastSuggestedSceneId === resolvedSceneId && lastSuggestedCharacterSignature === signature)) {
|
||||||
runtimeDebug("Character suggestions skipped.");
|
runtimeDebug("Character suggestions skipped.");
|
||||||
} else {
|
} else {
|
||||||
|
if (!isSnapshotFresh(snapshot)) {
|
||||||
|
runtimeDebug("Character suggestions skipped before submit (stale snapshot).");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const apiStartedAt = runtimePerformanceNow();
|
||||||
const response = await postJson("/api/word-companion/runtime/character-suggestions", {
|
const response = await postJson("/api/word-companion/runtime/character-suggestions", {
|
||||||
documentGuid: currentDocumentGuid(),
|
documentGuid: snapshot.documentGuid,
|
||||||
sceneId: resolvedSceneId,
|
sceneId: snapshot.sceneId,
|
||||||
characterIds
|
characterIds
|
||||||
});
|
});
|
||||||
|
runtimeTiming(`Character suggestion API: ${Math.round(runtimePerformanceNow() - apiStartedAt)}ms`);
|
||||||
|
if (!isSnapshotFresh(snapshot)) {
|
||||||
|
runtimeDebug("Character suggestion result ignored (stale snapshot).");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
lastSuggestedSceneId = resolvedSceneId;
|
lastSuggestedSceneId = snapshot.sceneId;
|
||||||
lastSuggestedCharacterSignature = signature;
|
lastSuggestedCharacterSignature = signature;
|
||||||
if ((response?.createdCount || 0) > 0) {
|
if ((response?.createdCount || 0) > 0) {
|
||||||
const names = detected.slice(0, 3).map((item) => item.name).join(", ");
|
const names = detected.slice(0, 3).map((item) => item.name).join(", ");
|
||||||
@ -4146,14 +4246,24 @@
|
|||||||
if (!assetSignature || (lastSuggestedAssetSceneId === resolvedSceneId && lastSuggestedAssetSignature === assetSignature)) {
|
if (!assetSignature || (lastSuggestedAssetSceneId === resolvedSceneId && lastSuggestedAssetSignature === assetSignature)) {
|
||||||
runtimeDebug("Asset suggestions skipped.");
|
runtimeDebug("Asset suggestions skipped.");
|
||||||
} else {
|
} else {
|
||||||
|
if (!isSnapshotFresh(snapshot)) {
|
||||||
|
runtimeDebug("Asset suggestions skipped before submit (stale snapshot).");
|
||||||
|
return;
|
||||||
|
}
|
||||||
runtimeDebug(`Asset matches found: ${detectedAssets.map((item) => item.name).join(", ")}`);
|
runtimeDebug(`Asset matches found: ${detectedAssets.map((item) => item.name).join(", ")}`);
|
||||||
|
const apiStartedAt = runtimePerformanceNow();
|
||||||
const assetResponse = await postJson("/api/word-companion/runtime/asset-suggestions", {
|
const assetResponse = await postJson("/api/word-companion/runtime/asset-suggestions", {
|
||||||
documentGuid: currentDocumentGuid(),
|
documentGuid: snapshot.documentGuid,
|
||||||
sceneId: resolvedSceneId,
|
sceneId: snapshot.sceneId,
|
||||||
assetIds
|
assetIds
|
||||||
});
|
});
|
||||||
|
runtimeTiming(`Asset suggestion API: ${Math.round(runtimePerformanceNow() - apiStartedAt)}ms`);
|
||||||
|
if (!isSnapshotFresh(snapshot)) {
|
||||||
|
runtimeDebug("Asset suggestion result ignored (stale snapshot).");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
lastSuggestedAssetSceneId = resolvedSceneId;
|
lastSuggestedAssetSceneId = snapshot.sceneId;
|
||||||
lastSuggestedAssetSignature = assetSignature;
|
lastSuggestedAssetSignature = assetSignature;
|
||||||
if ((assetResponse?.createdCount || 0) > 0) {
|
if ((assetResponse?.createdCount || 0) > 0) {
|
||||||
const names = detectedAssets.slice(0, 3).map((item) => item.name).join(", ");
|
const names = detectedAssets.slice(0, 3).map((item) => item.name).join(", ");
|
||||||
@ -4166,14 +4276,24 @@
|
|||||||
if (!locationSignature || (lastSuggestedLocationSceneId === resolvedSceneId && lastSuggestedLocationSignature === locationSignature)) {
|
if (!locationSignature || (lastSuggestedLocationSceneId === resolvedSceneId && lastSuggestedLocationSignature === locationSignature)) {
|
||||||
runtimeDebug("Location suggestions skipped.");
|
runtimeDebug("Location suggestions skipped.");
|
||||||
} else {
|
} else {
|
||||||
|
if (!isSnapshotFresh(snapshot)) {
|
||||||
|
runtimeDebug("Location suggestions skipped before submit (stale snapshot).");
|
||||||
|
return;
|
||||||
|
}
|
||||||
runtimeDebug(`Location matches found: ${detectedLocations.map((item) => item.name).join(", ")}`);
|
runtimeDebug(`Location matches found: ${detectedLocations.map((item) => item.name).join(", ")}`);
|
||||||
|
const apiStartedAt = runtimePerformanceNow();
|
||||||
const locationResponse = await postJson("/api/word-companion/runtime/location-suggestions", {
|
const locationResponse = await postJson("/api/word-companion/runtime/location-suggestions", {
|
||||||
documentGuid: currentDocumentGuid(),
|
documentGuid: snapshot.documentGuid,
|
||||||
sceneId: resolvedSceneId,
|
sceneId: snapshot.sceneId,
|
||||||
locationIds
|
locationIds
|
||||||
});
|
});
|
||||||
|
runtimeTiming(`Location suggestion API: ${Math.round(runtimePerformanceNow() - apiStartedAt)}ms`);
|
||||||
|
if (!isSnapshotFresh(snapshot)) {
|
||||||
|
runtimeDebug("Location suggestion result ignored (stale snapshot).");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
lastSuggestedLocationSceneId = resolvedSceneId;
|
lastSuggestedLocationSceneId = snapshot.sceneId;
|
||||||
lastSuggestedLocationSignature = locationSignature;
|
lastSuggestedLocationSignature = locationSignature;
|
||||||
if ((locationResponse?.createdCount || 0) > 0) {
|
if ((locationResponse?.createdCount || 0) > 0) {
|
||||||
const names = detectedLocations.slice(0, 3).map((item) => item.name).join(", ");
|
const names = detectedLocations.slice(0, 3).map((item) => item.name).join(", ");
|
||||||
@ -4182,6 +4302,7 @@
|
|||||||
}
|
}
|
||||||
runtimeDebug(locationResponse?.message || "Location suggestions submitted.");
|
runtimeDebug(locationResponse?.message || "Location suggestions submitted.");
|
||||||
}
|
}
|
||||||
|
runtimeTiming(`Total suggestion pass: ${Math.round(runtimePerformanceNow() - totalStartedAt)}ms`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Unable to detect runtime suggestions.", error);
|
console.error("Unable to detect runtime suggestions.", error);
|
||||||
setDiagnostics({ lastError: errorText(error) });
|
setDiagnostics({ lastError: errorText(error) });
|
||||||
@ -4522,10 +4643,13 @@
|
|||||||
runtimeDebug("Runtime update executed.");
|
runtimeDebug("Runtime update executed.");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const selectionParagraphs = await window.Word.run(async (context) => await readSelectionParagraphs(context));
|
const snapshot = await captureCurrentSceneSnapshot({ includeSelection: true });
|
||||||
const selectedIndex = findSelectedParagraphIndex(documentStructureCache, selectionParagraphs);
|
if (!isSnapshotFresh(snapshot)) {
|
||||||
const changed = applyRuntimeSelection(selectedIndex);
|
setSceneTrackingState("Waiting for typing to pause");
|
||||||
await recountCachedCurrentScene();
|
runtimeDebug("Runtime update skipped (stale snapshot).");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const changed = !!snapshot.selectionChanged;
|
||||||
|
|
||||||
if (!changed) {
|
if (!changed) {
|
||||||
setSceneContextStale(false);
|
setSceneContextStale(false);
|
||||||
@ -4570,8 +4694,16 @@
|
|||||||
const markRuntimeDirty = (reason = "selection", delayMs = runtimeIdleDelayMs) => {
|
const markRuntimeDirty = (reason = "selection", delayMs = runtimeIdleDelayMs) => {
|
||||||
runtimeSelectionDirty = true;
|
runtimeSelectionDirty = true;
|
||||||
runtimeLastActivityAt = Date.now();
|
runtimeLastActivityAt = Date.now();
|
||||||
|
runtimeGeneration += 1;
|
||||||
|
lastCurrentSceneSnapshot = null;
|
||||||
clearPendingSceneWordCountSync();
|
clearPendingSceneWordCountSync();
|
||||||
clearPendingCharacterSuggestionDetection();
|
clearPendingCharacterSuggestionDetection();
|
||||||
|
if (sceneWordCountSyncInFlight) {
|
||||||
|
sceneWordCountSyncPending = true;
|
||||||
|
}
|
||||||
|
if (characterSuggestionInFlight) {
|
||||||
|
characterSuggestionPending = true;
|
||||||
|
}
|
||||||
runtimeDebug(`${reason} changed.`);
|
runtimeDebug(`${reason} changed.`);
|
||||||
|
|
||||||
if (isAutoRefreshingScene) {
|
if (isAutoRefreshingScene) {
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Loading…
x
Reference in New Issue
Block a user