Phase 16F - Scene Word Count Sync and Writing Progress Update
This commit is contained in:
parent
85404b9c42
commit
d442de5837
@ -109,6 +109,13 @@ public sealed class WordCompanionController(IWordCompanionService wordCompanion)
|
|||||||
return response is null ? NotFound() : Ok(response);
|
return response is null ? NotFound() : Ok(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpPost("runtime/scene-word-count")]
|
||||||
|
public async Task<IActionResult> UpdateRuntimeSceneWordCount(WordCompanionRuntimeSceneWordCountRequest request)
|
||||||
|
{
|
||||||
|
var response = await wordCompanion.UpdateRuntimeSceneWordCountAsync(request);
|
||||||
|
return response is null ? BadRequest() : 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)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -19,6 +19,7 @@ public interface IWordCompanionRepository
|
|||||||
Task<WordCompanionSyncManuscriptResponse?> SyncManuscriptAsync(int bookId, int userId, WordCompanionSyncManuscriptRequest request);
|
Task<WordCompanionSyncManuscriptResponse?> SyncManuscriptAsync(int bookId, int userId, WordCompanionSyncManuscriptRequest request);
|
||||||
Task<WordCompanionUpdateSceneLinksResponse?> UpdateSceneLinksAsync(int sceneId, int userId, WordCompanionUpdateSceneLinksRequest request);
|
Task<WordCompanionUpdateSceneLinksResponse?> UpdateSceneLinksAsync(int sceneId, int userId, WordCompanionUpdateSceneLinksRequest request);
|
||||||
Task<WordCompanionUpdateWordCountResponse?> UpdateWordCountAsync(int sceneId, int userId, int actualWords, DateTime? lastWorkedOn);
|
Task<WordCompanionUpdateWordCountResponse?> UpdateWordCountAsync(int sceneId, int userId, int actualWords, DateTime? lastWorkedOn);
|
||||||
|
Task<WordCompanionUpdateWordCountResponse?> UpdateRuntimeSceneWordCountAsync(int userId, WordCompanionRuntimeSceneWordCountRequest request);
|
||||||
Task<WordCompanionManuscriptAnalyseResponse?> AnalyseManuscriptAsync(int userId, WordCompanionManuscriptAnalyseRequest request);
|
Task<WordCompanionManuscriptAnalyseResponse?> AnalyseManuscriptAsync(int userId, WordCompanionManuscriptAnalyseRequest request);
|
||||||
Task<WordCompanionManuscriptImportResponse?> ImportManuscriptAsync(int userId, WordCompanionManuscriptImportRequest request);
|
Task<WordCompanionManuscriptImportResponse?> ImportManuscriptAsync(int userId, WordCompanionManuscriptImportRequest request);
|
||||||
Task<bool> HasProjectAccessAsync(int projectId, int userId);
|
Task<bool> HasProjectAccessAsync(int projectId, int userId);
|
||||||
@ -235,6 +236,23 @@ public sealed class WordCompanionRepository(ISqlConnectionFactory connectionFact
|
|||||||
commandType: CommandType.StoredProcedure);
|
commandType: CommandType.StoredProcedure);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<WordCompanionUpdateWordCountResponse?> UpdateRuntimeSceneWordCountAsync(int userId, WordCompanionRuntimeSceneWordCountRequest request)
|
||||||
|
{
|
||||||
|
using var connection = connectionFactory.CreateConnection();
|
||||||
|
return await connection.QuerySingleOrDefaultAsync<WordCompanionUpdateWordCountResponse>(
|
||||||
|
"dbo.WordCompanion_Runtime_SceneWordCount_Update",
|
||||||
|
new
|
||||||
|
{
|
||||||
|
UserID = userId,
|
||||||
|
request.DocumentGuid,
|
||||||
|
request.BookId,
|
||||||
|
request.ChapterId,
|
||||||
|
request.SceneId,
|
||||||
|
request.WordCount
|
||||||
|
},
|
||||||
|
commandType: CommandType.StoredProcedure);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<WordCompanionManuscriptAnalyseResponse?> AnalyseManuscriptAsync(int userId, WordCompanionManuscriptAnalyseRequest request)
|
public async Task<WordCompanionManuscriptAnalyseResponse?> AnalyseManuscriptAsync(int userId, WordCompanionManuscriptAnalyseRequest request)
|
||||||
{
|
{
|
||||||
using var connection = connectionFactory.CreateConnection();
|
using var connection = connectionFactory.CreateConnection();
|
||||||
|
|||||||
@ -219,6 +219,15 @@ public sealed class WordCompanionUpdateWordCountResponse
|
|||||||
public string Message { get; init; } = "Word count updated.";
|
public string Message { get; init; } = "Word count updated.";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public sealed class WordCompanionRuntimeSceneWordCountRequest
|
||||||
|
{
|
||||||
|
public Guid DocumentGuid { get; set; }
|
||||||
|
public int BookId { get; set; }
|
||||||
|
public int ChapterId { get; set; }
|
||||||
|
public int SceneId { get; set; }
|
||||||
|
public int WordCount { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
public sealed class WordCompanionManuscriptDocumentDto
|
public sealed class WordCompanionManuscriptDocumentDto
|
||||||
{
|
{
|
||||||
public int ManuscriptDocumentId { get; init; }
|
public int ManuscriptDocumentId { get; init; }
|
||||||
|
|||||||
@ -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<WordCompanionUpdateWordCountResponse?> UpdateRuntimeSceneWordCountAsync(WordCompanionRuntimeSceneWordCountRequest request);
|
||||||
Task<WordCompanionRuntimeBookResponse?> GetRuntimeBookAsync(int bookId, Guid documentGuid);
|
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);
|
||||||
@ -222,6 +223,17 @@ public sealed class WordCompanionService(
|
|||||||
: repository.UpdateWordCountAsync(sceneId, RequireUserId(), actualWords.Value, request.LastWorkedOn);
|
: repository.UpdateWordCountAsync(sceneId, RequireUserId(), actualWords.Value, request.LastWorkedOn);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Task<WordCompanionUpdateWordCountResponse?> UpdateRuntimeSceneWordCountAsync(WordCompanionRuntimeSceneWordCountRequest request)
|
||||||
|
{
|
||||||
|
return request.DocumentGuid == Guid.Empty
|
||||||
|
|| request.BookId <= 0
|
||||||
|
|| request.ChapterId <= 0
|
||||||
|
|| request.SceneId <= 0
|
||||||
|
|| request.WordCount < 0
|
||||||
|
? Task.FromResult<WordCompanionUpdateWordCountResponse?>(null)
|
||||||
|
: repository.UpdateRuntimeSceneWordCountAsync(RequireUserId(), request);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<WordCompanionRuntimeBookResponse?> GetRuntimeBookAsync(int bookId, Guid documentGuid)
|
public async Task<WordCompanionRuntimeBookResponse?> GetRuntimeBookAsync(int bookId, Guid documentGuid)
|
||||||
{
|
{
|
||||||
if (bookId <= 0 || documentGuid == Guid.Empty)
|
if (bookId <= 0 || documentGuid == Guid.Empty)
|
||||||
|
|||||||
@ -0,0 +1,78 @@
|
|||||||
|
CREATE OR ALTER PROCEDURE dbo.WordCompanion_Runtime_SceneWordCount_Update
|
||||||
|
@UserID int,
|
||||||
|
@DocumentGuid uniqueidentifier,
|
||||||
|
@BookID int,
|
||||||
|
@ChapterID int,
|
||||||
|
@SceneID int,
|
||||||
|
@WordCount int
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
|
||||||
|
IF @DocumentGuid IS NULL
|
||||||
|
OR @DocumentGuid = '00000000-0000-0000-0000-000000000000'
|
||||||
|
OR @BookID <= 0
|
||||||
|
OR @ChapterID <= 0
|
||||||
|
OR @SceneID <= 0
|
||||||
|
OR @WordCount < 0
|
||||||
|
RETURN;
|
||||||
|
|
||||||
|
DECLARE @ManuscriptDocumentID int;
|
||||||
|
|
||||||
|
SELECT @ManuscriptDocumentID = md.ManuscriptDocumentID
|
||||||
|
FROM dbo.ManuscriptDocuments md
|
||||||
|
INNER JOIN dbo.Books b ON b.BookID = md.BookID
|
||||||
|
INNER JOIN dbo.Projects p ON p.ProjectID = b.ProjectID
|
||||||
|
INNER JOIN dbo.ProjectUserAccess pua ON pua.ProjectID = p.ProjectID
|
||||||
|
WHERE md.DocumentGuid = @DocumentGuid
|
||||||
|
AND md.BookID = @BookID
|
||||||
|
AND md.IsActive = 1
|
||||||
|
AND b.IsArchived = 0
|
||||||
|
AND p.IsArchived = 0
|
||||||
|
AND pua.UserID = @UserID
|
||||||
|
AND pua.IsActive = 1;
|
||||||
|
|
||||||
|
IF @ManuscriptDocumentID IS NULL
|
||||||
|
RETURN;
|
||||||
|
|
||||||
|
IF NOT EXISTS
|
||||||
|
(
|
||||||
|
SELECT 1
|
||||||
|
FROM dbo.Scenes s
|
||||||
|
INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID
|
||||||
|
WHERE s.SceneID = @SceneID
|
||||||
|
AND s.ChapterID = @ChapterID
|
||||||
|
AND c.BookID = @BookID
|
||||||
|
AND s.IsArchived = 0
|
||||||
|
AND c.IsArchived = 0
|
||||||
|
AND s.IsLinkedToManuscript = 1
|
||||||
|
AND s.ManuscriptDocumentID = @ManuscriptDocumentID
|
||||||
|
)
|
||||||
|
RETURN;
|
||||||
|
|
||||||
|
DECLARE @LastWorkedOnDate date = CONVERT(date, SYSUTCDATETIME());
|
||||||
|
|
||||||
|
IF EXISTS (SELECT 1 FROM dbo.SceneWorkflow WHERE SceneID = @SceneID)
|
||||||
|
BEGIN
|
||||||
|
UPDATE dbo.SceneWorkflow
|
||||||
|
SET ActualWordCount = @WordCount,
|
||||||
|
LastWorkedOn = @LastWorkedOnDate,
|
||||||
|
UpdatedDate = SYSUTCDATETIME()
|
||||||
|
WHERE SceneID = @SceneID;
|
||||||
|
END
|
||||||
|
ELSE
|
||||||
|
BEGIN
|
||||||
|
INSERT dbo.SceneWorkflow (SceneID, ActualWordCount, LastWorkedOn)
|
||||||
|
VALUES (@SceneID, @WordCount, @LastWorkedOnDate);
|
||||||
|
END;
|
||||||
|
|
||||||
|
UPDATE dbo.ManuscriptDocuments
|
||||||
|
SET LastSyncUtc = SYSUTCDATETIME()
|
||||||
|
WHERE ManuscriptDocumentID = @ManuscriptDocumentID;
|
||||||
|
|
||||||
|
SELECT @SceneID AS SceneId,
|
||||||
|
@WordCount AS ActualWords,
|
||||||
|
@LastWorkedOnDate AS LastWorkedOn,
|
||||||
|
N'Word count synced.' AS Message;
|
||||||
|
END;
|
||||||
|
GO
|
||||||
@ -116,8 +116,8 @@
|
|||||||
|
|
||||||
<section class="word-companion-section word-companion-current-scene" aria-label="Current Scene">
|
<section class="word-companion-section word-companion-current-scene" aria-label="Current Scene">
|
||||||
<div class="word-companion-actions">
|
<div class="word-companion-actions">
|
||||||
<button type="button" class="word-companion-primary-action" data-refresh-current-scene>Refresh Current Scene</button>
|
<button type="button" class="word-companion-primary-action" data-sync-scene-progress disabled>Sync Current Scene</button>
|
||||||
<button type="button" data-sync-scene-progress disabled>Sync Progress</button>
|
<button type="button" data-refresh-current-scene>Refresh Current Scene</button>
|
||||||
</div>
|
</div>
|
||||||
<p class="word-companion-message" data-document-message></p>
|
<p class="word-companion-message" data-document-message></p>
|
||||||
<p class="word-companion-message" data-sync-status>No resolved PlotDirector scene.</p>
|
<p class="word-companion-message" data-sync-status>No resolved PlotDirector scene.</p>
|
||||||
|
|||||||
@ -198,6 +198,10 @@
|
|||||||
let selectionRefreshTimer = null;
|
let selectionRefreshTimer = null;
|
||||||
let selectionHandlerRegistered = false;
|
let selectionHandlerRegistered = false;
|
||||||
let isAutoRefreshingScene = false;
|
let isAutoRefreshingScene = false;
|
||||||
|
let sceneWordCountSyncTimer = null;
|
||||||
|
let sceneWordCountSyncInFlight = false;
|
||||||
|
let lastSyncedSceneId = null;
|
||||||
|
let lastSyncedWordCount = null;
|
||||||
let lastLinkedSceneTitle = "";
|
let lastLinkedSceneTitle = "";
|
||||||
let lastLinkedChapterTitle = "";
|
let lastLinkedChapterTitle = "";
|
||||||
let currentSceneLinks = {
|
let currentSceneLinks = {
|
||||||
@ -353,6 +357,13 @@
|
|||||||
updateAnchorDisplay();
|
updateAnchorDisplay();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const clearPendingSceneWordCountSync = () => {
|
||||||
|
if (sceneWordCountSyncTimer) {
|
||||||
|
window.clearTimeout(sceneWordCountSyncTimer);
|
||||||
|
sceneWordCountSyncTimer = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const setHidden = (element, hidden) => {
|
const setHidden = (element, hidden) => {
|
||||||
if (element) {
|
if (element) {
|
||||||
element.hidden = hidden;
|
element.hidden = hidden;
|
||||||
@ -410,8 +421,9 @@
|
|||||||
|
|
||||||
const updateSceneActionButtons = () => {
|
const updateSceneActionButtons = () => {
|
||||||
const blocked = sceneContextStale || !resolvedSceneId;
|
const blocked = sceneContextStale || !resolvedSceneId;
|
||||||
|
const syncBlocked = blocked || !resolvedChapterId || sceneWordCountSyncInFlight;
|
||||||
if (syncSceneProgressButton) {
|
if (syncSceneProgressButton) {
|
||||||
syncSceneProgressButton.disabled = blocked;
|
syncSceneProgressButton.disabled = syncBlocked;
|
||||||
}
|
}
|
||||||
if (saveSceneLinksButton) {
|
if (saveSceneLinksButton) {
|
||||||
saveSceneLinksButton.disabled = blocked;
|
saveSceneLinksButton.disabled = blocked;
|
||||||
@ -435,6 +447,9 @@
|
|||||||
|
|
||||||
const setSceneContextStale = (isStale, message = "") => {
|
const setSceneContextStale = (isStale, message = "") => {
|
||||||
sceneContextStale = !!isStale;
|
sceneContextStale = !!isStale;
|
||||||
|
if (sceneContextStale) {
|
||||||
|
clearPendingSceneWordCountSync();
|
||||||
|
}
|
||||||
setText(sceneTrackingStatus, sceneContextStale ? "Stale" : sceneTrackingMode);
|
setText(sceneTrackingStatus, sceneContextStale ? "Stale" : sceneTrackingMode);
|
||||||
updateSceneActionButtons();
|
updateSceneActionButtons();
|
||||||
if (message) {
|
if (message) {
|
||||||
@ -447,6 +462,12 @@
|
|||||||
resolvedSceneId = Number.isInteger(sceneId) && sceneId > 0 ? sceneId : null;
|
resolvedSceneId = Number.isInteger(sceneId) && sceneId > 0 ? sceneId : null;
|
||||||
resolvedChapterId = Number.isInteger(chapterId) && chapterId > 0 ? chapterId : null;
|
resolvedChapterId = Number.isInteger(chapterId) && chapterId > 0 ? chapterId : null;
|
||||||
currentResolutionMethod = resolutionMethod || "";
|
currentResolutionMethod = resolutionMethod || "";
|
||||||
|
if (resolvedSceneId !== lastSyncedSceneId) {
|
||||||
|
lastSyncedWordCount = null;
|
||||||
|
}
|
||||||
|
if (!resolvedSceneId) {
|
||||||
|
clearPendingSceneWordCountSync();
|
||||||
|
}
|
||||||
updateSceneActionButtons();
|
updateSceneActionButtons();
|
||||||
setSyncStatus(sceneContextStale
|
setSyncStatus(sceneContextStale
|
||||||
? "Refresh Current Scene before syncing."
|
? "Refresh Current Scene before syncing."
|
||||||
@ -484,10 +505,28 @@
|
|||||||
return Number.isNaN(date.getTime()) ? "-" : date.toLocaleString();
|
return Number.isNaN(date.getTime()) ? "-" : date.toLocaleString();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const formatLastSynced = (value) => {
|
||||||
|
if (!value) {
|
||||||
|
return "-";
|
||||||
|
}
|
||||||
|
|
||||||
|
const date = value instanceof Date ? value : new Date(value);
|
||||||
|
if (Number.isNaN(date.getTime())) {
|
||||||
|
return "-";
|
||||||
|
}
|
||||||
|
|
||||||
|
const today = new Date();
|
||||||
|
const dayLabel = date.toDateString() === today.toDateString()
|
||||||
|
? "Today"
|
||||||
|
: date.toLocaleDateString();
|
||||||
|
return `Last synced: ${dayLabel} ${date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}`;
|
||||||
|
};
|
||||||
|
|
||||||
const setRuntimeBookContext = (context) => {
|
const setRuntimeBookContext = (context) => {
|
||||||
runtimeBookContext = context || null;
|
runtimeBookContext = context || null;
|
||||||
setText(runtimeLastSync, formatDateTime(runtimeBookContext?.lastSyncUtc || runtimeBookContext?.manuscriptDocument?.lastSyncUtc));
|
const lastSyncUtc = runtimeBookContext?.lastSyncUtc || runtimeBookContext?.manuscriptDocument?.lastSyncUtc;
|
||||||
setText(syncLastSynced, formatDateTime(runtimeBookContext?.lastSyncUtc || runtimeBookContext?.manuscriptDocument?.lastSyncUtc));
|
setText(runtimeLastSync, formatLastSynced(lastSyncUtc));
|
||||||
|
setText(syncLastSynced, formatLastSynced(lastSyncUtc));
|
||||||
};
|
};
|
||||||
|
|
||||||
const clearRuntimeCaches = () => {
|
const clearRuntimeCaches = () => {
|
||||||
@ -847,6 +886,7 @@
|
|||||||
hideSceneCreateLink();
|
hideSceneCreateLink();
|
||||||
setResolvedScene(sceneId, chapterId, resolutionMethod);
|
setResolvedScene(sceneId, chapterId, resolutionMethod);
|
||||||
renderCompanionData(companionData);
|
renderCompanionData(companionData);
|
||||||
|
scheduleSceneWordCountSync();
|
||||||
};
|
};
|
||||||
|
|
||||||
const setText = (element, value) => {
|
const setText = (element, value) => {
|
||||||
@ -1260,6 +1300,8 @@
|
|||||||
(Number(char) ^ Math.random() * 16 >> Number(char) / 4).toString(16));
|
(Number(char) ^ Math.random() * 16 >> Number(char) / 4).toString(16));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const currentDocumentGuid = () => firstRunDocumentBinding?.documentGuid || readDocumentBinding()?.documentGuid || "";
|
||||||
|
|
||||||
const updateSummary = () => {
|
const updateSummary = () => {
|
||||||
const project = selectedProject();
|
const project = selectedProject();
|
||||||
const book = selectedBook();
|
const book = selectedBook();
|
||||||
@ -2097,6 +2139,45 @@
|
|||||||
return selectionParagraphs.items;
|
return selectionParagraphs.items;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const recountCachedCurrentScene = async () => {
|
||||||
|
const scene = documentStructureCache?.currentScene;
|
||||||
|
if (!scene || !Number.isInteger(scene.startParagraphIndex) || !Number.isInteger(scene.endParagraphIndex)) {
|
||||||
|
return detectedSceneWordCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!officeReady || !wordHostAvailable || !window.Word || typeof window.Word.run !== "function") {
|
||||||
|
return detectedSceneWordCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await window.Word.run(async (context) => {
|
||||||
|
const bodyParagraphs = context.document.body.paragraphs;
|
||||||
|
bodyParagraphs.load("text");
|
||||||
|
await context.sync();
|
||||||
|
|
||||||
|
let wordCount = 0;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
wordCount += countWords(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
scene.wordCount = wordCount;
|
||||||
|
setCurrentStructure(
|
||||||
|
documentStructureCache.currentChapter?.title,
|
||||||
|
scene.title,
|
||||||
|
wordCount,
|
||||||
|
documentStructureCache.currentChapter?.anchorId,
|
||||||
|
scene.anchorId,
|
||||||
|
documentStructureCache.currentChapter?.index
|
||||||
|
);
|
||||||
|
return wordCount;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const refreshDocumentStructure = async () => {
|
const refreshDocumentStructure = async () => {
|
||||||
setDocumentMessage("");
|
setDocumentMessage("");
|
||||||
|
|
||||||
@ -3568,13 +3649,52 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!resolvedChapterId) {
|
||||||
|
setSyncStatus("No resolved PlotDirector chapter.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (sceneContextStale) {
|
if (sceneContextStale) {
|
||||||
setSyncStatus("Refresh Current Scene before syncing.");
|
setSyncStatus("Refresh Current Scene before syncing.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Number.isInteger(detectedSceneWordCount)) {
|
const book = selectedBook();
|
||||||
setSyncStatus("Unable to sync progress.");
|
const documentGuid = currentDocumentGuid();
|
||||||
|
if (!book || !documentGuid) {
|
||||||
|
setSyncStatus("Bound manuscript metadata is unavailable.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sceneWordCountSyncInFlight) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
clearPendingSceneWordCountSync();
|
||||||
|
sceneWordCountSyncInFlight = true;
|
||||||
|
|
||||||
|
let wordCount = detectedSceneWordCount;
|
||||||
|
try {
|
||||||
|
wordCount = await recountCachedCurrentScene();
|
||||||
|
} catch (error) {
|
||||||
|
setSyncStatus("Sync failed");
|
||||||
|
setDiagnostics({ lastError: errorText(error) });
|
||||||
|
sceneWordCountSyncInFlight = false;
|
||||||
|
updateSceneActionButtons();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Number.isInteger(wordCount) || wordCount < 0) {
|
||||||
|
setSyncStatus("Sync failed");
|
||||||
|
sceneWordCountSyncInFlight = false;
|
||||||
|
updateSceneActionButtons();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastSyncedSceneId === resolvedSceneId && lastSyncedWordCount === wordCount) {
|
||||||
|
setSyncStatus("Synced");
|
||||||
|
sceneWordCountSyncInFlight = false;
|
||||||
|
updateSceneActionButtons();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -3583,26 +3703,48 @@
|
|||||||
syncSceneProgressButton.textContent = "Syncing...";
|
syncSceneProgressButton.textContent = "Syncing...";
|
||||||
}
|
}
|
||||||
|
|
||||||
const lastWorkedOn = new Date().toISOString();
|
setSyncStatus("Syncing...");
|
||||||
try {
|
try {
|
||||||
const response = await patchJson(`/api/word-companion/scenes/${resolvedSceneId}/word-count`, {
|
const response = await postJson("/api/word-companion/runtime/scene-word-count", {
|
||||||
actualWordCount: detectedSceneWordCount,
|
documentGuid,
|
||||||
lastWorkedOn
|
bookId: book.bookId,
|
||||||
|
chapterId: resolvedChapterId,
|
||||||
|
sceneId: resolvedSceneId,
|
||||||
|
wordCount
|
||||||
});
|
});
|
||||||
setText(syncPlotDirectorCount, displayValue(response?.actualWords ?? detectedSceneWordCount));
|
const actualWords = response?.actualWords ?? wordCount;
|
||||||
setText(plotDirectorActualWords, displayValue(response?.actualWords ?? detectedSceneWordCount));
|
const syncedAt = new Date();
|
||||||
setText(syncLastSynced, new Date().toLocaleString());
|
lastSyncedSceneId = resolvedSceneId;
|
||||||
setSyncStatus("Progress synced successfully.");
|
lastSyncedWordCount = actualWords;
|
||||||
} catch {
|
setText(syncPlotDirectorCount, displayValue(actualWords));
|
||||||
setSyncStatus("Unable to sync progress.");
|
setText(plotDirectorActualWords, displayValue(actualWords));
|
||||||
|
setText(syncLastSynced, formatLastSynced(syncedAt));
|
||||||
|
setText(runtimeLastSync, formatLastSynced(syncedAt));
|
||||||
|
setSyncStatus("Synced");
|
||||||
|
} catch (error) {
|
||||||
|
setSyncStatus("Sync failed");
|
||||||
|
setDiagnostics({ lastError: errorText(error) });
|
||||||
} finally {
|
} finally {
|
||||||
|
sceneWordCountSyncInFlight = false;
|
||||||
if (syncSceneProgressButton) {
|
if (syncSceneProgressButton) {
|
||||||
syncSceneProgressButton.disabled = !resolvedSceneId || sceneContextStale;
|
syncSceneProgressButton.textContent = "Sync Current Scene";
|
||||||
syncSceneProgressButton.textContent = "Sync Progress";
|
|
||||||
}
|
}
|
||||||
|
updateSceneActionButtons();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const scheduleSceneWordCountSync = (delayMs = 4000) => {
|
||||||
|
clearPendingSceneWordCountSync();
|
||||||
|
if (!resolvedSceneId || !resolvedChapterId || sceneContextStale || sceneWordCountSyncInFlight) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sceneWordCountSyncTimer = window.setTimeout(() => {
|
||||||
|
sceneWordCountSyncTimer = null;
|
||||||
|
void syncSceneProgress();
|
||||||
|
}, delayMs);
|
||||||
|
};
|
||||||
|
|
||||||
const refreshPlotDirectorChapter = async () => {
|
const refreshPlotDirectorChapter = async () => {
|
||||||
const project = selectedProject();
|
const project = selectedProject();
|
||||||
const book = selectedBook();
|
const book = selectedBook();
|
||||||
@ -3907,6 +4049,7 @@
|
|||||||
if (!changed) {
|
if (!changed) {
|
||||||
setSceneContextStale(false);
|
setSceneContextStale(false);
|
||||||
setSceneTrackingState("Live");
|
setSceneTrackingState("Live");
|
||||||
|
scheduleSceneWordCountSync();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -3918,6 +4061,7 @@
|
|||||||
setSceneContextStale(false);
|
setSceneContextStale(false);
|
||||||
setSceneTrackingState("Live");
|
setSceneTrackingState("Live");
|
||||||
setDocumentMessage("");
|
setDocumentMessage("");
|
||||||
|
scheduleSceneWordCountSync();
|
||||||
} 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({ lastError: errorText(error) });
|
setDiagnostics({ lastError: errorText(error) });
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Loading…
x
Reference in New Issue
Block a user