diff --git a/PlotLine/Views/WordCompanionHost/Index.cshtml b/PlotLine/Views/WordCompanionHost/Index.cshtml index 0758a89..21d57e4 100644 --- a/PlotLine/Views/WordCompanionHost/Index.cshtml +++ b/PlotLine/Views/WordCompanionHost/Index.cshtml @@ -75,6 +75,59 @@ + + + PlotDirector Scene + Refresh PlotDirector Scene + + Scene Status + Select a PlotDirector book. + + + Scene Title + - + + + Revision Status + - + + + Estimated Words + - + + + Actual Words + - + + + Blocked Status + - + + + Blocked Reason + - + + + + Writing Brief + + + + + Characters + + + + Assets + + + + Locations + + + + + Diagnostics diff --git a/PlotLine/wwwroot/css/word-companion.css b/PlotLine/wwwroot/css/word-companion.css index 11fbe7b..66a3394 100644 --- a/PlotLine/wwwroot/css/word-companion.css +++ b/PlotLine/wwwroot/css/word-companion.css @@ -174,6 +174,60 @@ body { font-size: 0.88rem; } +.word-companion-scene-details, +.word-companion-link-groups { + display: grid; + gap: 8px; +} + +.word-companion-scene-details div, +.word-companion-link-groups div { + display: grid; + gap: 3px; +} + +.word-companion-brief { + display: grid; + gap: 5px; + border-top: 1px solid var(--companion-line); + padding-top: 8px; +} + +.word-companion-brief p { + margin: 0; + border: 1px solid var(--companion-line); + border-radius: 6px; + padding: 8px; + background: var(--companion-soft); + color: var(--companion-ink); + font-size: 0.9rem; + line-height: 1.35; + white-space: pre-wrap; +} + +.word-companion-chip-list { + display: flex; + flex-wrap: wrap; + gap: 5px; +} + +.word-companion-chip, +.word-companion-empty-chip { + border: 1px solid rgba(47, 111, 99, 0.25); + border-radius: 999px; + padding: 3px 7px; + background: var(--companion-soft); + color: var(--companion-accent-dark); + font-size: 0.82rem; + font-weight: 700; +} + +.word-companion-empty-chip { + border-color: var(--companion-line); + color: var(--companion-muted); + font-weight: 600; +} + .word-companion-outline { display: grid; gap: 8px; diff --git a/PlotLine/wwwroot/js/word-companion-host.js b/PlotLine/wwwroot/js/word-companion-host.js index 837975e..3c44693 100644 --- a/PlotLine/wwwroot/js/word-companion-host.js +++ b/PlotLine/wwwroot/js/word-companion-host.js @@ -21,6 +21,22 @@ const documentMessage = document.querySelector("[data-document-message]"); const syncNote = document.querySelector("[data-sync-note]"); const documentOutline = document.querySelector("[data-document-outline]"); + const refreshPlotDirectorSceneButton = document.querySelector("[data-refresh-plotdirector-scene]"); + const plotDirectorSceneStatus = document.querySelector("[data-plotdirector-scene-status]"); + const plotDirectorSceneDetails = document.querySelector("[data-plotdirector-scene-details]"); + const plotDirectorSceneTitle = document.querySelector("[data-plotdirector-scene-title]"); + const plotDirectorRevisionStatus = document.querySelector("[data-plotdirector-revision-status]"); + const plotDirectorEstimatedWords = document.querySelector("[data-plotdirector-estimated-words]"); + const plotDirectorActualWords = document.querySelector("[data-plotdirector-actual-words]"); + 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 writingBriefBlock = document.querySelector("[data-writing-brief-block]"); + const writingBrief = document.querySelector("[data-writing-brief]"); + const plotDirectorLinkGroups = document.querySelector("[data-plotdirector-link-groups]"); + const characterChips = document.querySelector("[data-character-chips]"); + const assetChips = document.querySelector("[data-asset-chips]"); + const locationChips = document.querySelector("[data-location-chips]"); const runDiagnosticsButton = document.querySelector("[data-run-diagnostics]"); const diagnosticHost = document.querySelector("[data-diagnostic-host]"); const diagnosticPlatform = document.querySelector("[data-diagnostic-platform]"); @@ -37,6 +53,8 @@ let wordHostAvailable = false; let officeHost = "Unknown"; let officePlatform = "Unknown"; + let detectedChapterTitle = ""; + let detectedSceneTitle = ""; const setOfficeStatus = (message) => { if (officeStatus) { @@ -71,7 +89,16 @@ } }; + const setPlotDirectorSceneStatus = (message) => { + if (plotDirectorSceneStatus) { + plotDirectorSceneStatus.textContent = message; + } + }; + const setCurrentStructure = (chapterTitle, sceneTitle, wordCount) => { + detectedChapterTitle = chapterTitle || ""; + detectedSceneTitle = sceneTitle || ""; + if (currentChapter) { currentChapter.textContent = chapterTitle || "Not detected"; } @@ -83,6 +110,91 @@ } }; + const setHidden = (element, hidden) => { + if (element) { + element.hidden = hidden; + } + }; + + const displayValue = (value) => { + if (value === null || value === undefined || value === "") { + return "-"; + } + + return String(value); + }; + + const resetPlotDirectorScene = (message) => { + setPlotDirectorSceneStatus(message); + setHidden(plotDirectorSceneDetails, true); + setHidden(writingBriefBlock, true); + setHidden(plotDirectorLinkGroups, true); + }; + + const renderLinkedItems = (container, items) => { + if (!container) { + return; + } + + container.innerHTML = ""; + const linkedItems = Array.isArray(items) ? items.filter((item) => item.linked) : []; + if (linkedItems.length === 0) { + const empty = document.createElement("span"); + empty.className = "word-companion-empty-chip"; + empty.textContent = "None"; + container.append(empty); + return; + } + + for (const item of linkedItems) { + const chip = document.createElement("span"); + chip.className = "word-companion-chip"; + chip.textContent = item.name || "Unnamed"; + container.append(chip); + } + }; + + const renderCompanionData = (data) => { + setText(plotDirectorSceneTitle, displayValue(data?.sceneTitle)); + setText(plotDirectorRevisionStatus, displayValue(data?.revisionStatus || data?.revisionStatusName || "Not provided")); + setText(plotDirectorEstimatedWords, displayValue(data?.estimatedWords)); + setText(plotDirectorActualWords, displayValue(data?.actualWords)); + setText(plotDirectorBlockedStatus, data?.blocked ? "Blocked" : "Not blocked"); + + if (data?.blockedReason) { + setText(plotDirectorBlockedReason, data.blockedReason); + setHidden(plotDirectorBlockedReasonRow, false); + } else { + setText(plotDirectorBlockedReason, "-"); + setHidden(plotDirectorBlockedReasonRow, true); + } + + if (writingBrief) { + writingBrief.textContent = data?.writingBrief || "No writing brief available."; + } + + renderLinkedItems(characterChips, data?.characters); + renderLinkedItems(assetChips, data?.assets); + renderLinkedItems(locationChips, data?.locations); + setHidden(plotDirectorSceneDetails, false); + setHidden(writingBriefBlock, false); + setHidden(plotDirectorLinkGroups, false); + }; + + const findSceneRevisionStatus = async (bookId, sceneId) => { + const structure = await fetchJson(`/api/word-companion/books/${bookId}/structure`); + const chapters = Array.isArray(structure?.chapters) ? structure.chapters : []; + for (const chapter of chapters) { + const scenes = Array.isArray(chapter.scenes) ? chapter.scenes : []; + const scene = scenes.find((item) => item.sceneId === sceneId); + if (scene) { + return scene.revisionStatus || ""; + } + } + + return ""; + }; + const setText = (element, value) => { if (element) { element.textContent = value; @@ -477,10 +589,14 @@ } }; - const fetchJson = async (url) => { + const fetchJson = async (url, options = {}) => { const response = await window.fetch(url, { credentials: "same-origin", - headers: { Accept: "application/json" } + ...options, + headers: { + Accept: "application/json", + ...(options.headers || {}) + } }); if (!response.ok) { @@ -490,6 +606,72 @@ return await response.json(); }; + const postJson = (url, body) => fetchJson(url, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body) + }); + + const refreshPlotDirectorScene = async () => { + const book = selectedBook(); + if (!book) { + resetPlotDirectorScene("Select a PlotDirector book."); + return; + } + + if (!detectedChapterTitle || !detectedSceneTitle) { + resetPlotDirectorScene("No scene detected in Word."); + return; + } + + if (refreshPlotDirectorSceneButton) { + refreshPlotDirectorSceneButton.disabled = true; + refreshPlotDirectorSceneButton.textContent = "Refreshing..."; + } + const restorePlotDirectorSceneButton = () => { + if (refreshPlotDirectorSceneButton) { + refreshPlotDirectorSceneButton.disabled = false; + refreshPlotDirectorSceneButton.textContent = "Refresh PlotDirector Scene"; + } + }; + resetPlotDirectorScene("Resolving PlotDirector scene..."); + + let resolvedSceneId = null; + try { + const resolveResponse = await postJson(`/api/word-companion/books/${book.bookId}/resolve-scene`, { + chapterTitle: detectedChapterTitle, + sceneTitle: detectedSceneTitle + }); + + if (!resolveResponse?.matched || !resolveResponse.sceneId) { + resetPlotDirectorScene("Scene not found in PlotDirector."); + restorePlotDirectorSceneButton(); + return; + } + + resolvedSceneId = resolveResponse.sceneId; + } catch { + resetPlotDirectorScene("Unable to resolve scene."); + restorePlotDirectorSceneButton(); + return; + } + + try { + const companionData = await fetchJson(`/api/word-companion/scenes/${resolvedSceneId}/companion`); + try { + companionData.revisionStatus = await findSceneRevisionStatus(book.bookId, resolvedSceneId); + } catch { + companionData.revisionStatus = ""; + } + setPlotDirectorSceneStatus("Linked to PlotDirector"); + renderCompanionData(companionData); + } catch { + resetPlotDirectorScene("Unable to load PlotDirector scene data."); + } finally { + restorePlotDirectorSceneButton(); + } + }; + const loadBooks = async (projectId, preferredBookId) => { if (!projectId) { books = []; @@ -497,11 +679,13 @@ setBookMessage(""); writeStoredId(storageKeys.bookId, null); updateSummary(); + resetPlotDirectorScene("Select a PlotDirector book."); return; } resetSelect(bookSelect, "Loading books..."); setBookMessage(""); + resetPlotDirectorScene("Select a PlotDirector book."); try { const data = await fetchJson(`/api/word-companion/projects/${projectId}/books`); @@ -524,12 +708,14 @@ writeStoredId(storageKeys.bookId, null); } updateSummary(); + resetPlotDirectorScene(selectedBook() ? "No scene detected in Word." : "Select a PlotDirector book."); } catch { books = []; resetSelect(bookSelect, "Unable to load books."); setBookMessage("Unable to load books."); writeStoredId(storageKeys.bookId, null); updateSummary(); + resetPlotDirectorScene("Select a PlotDirector book."); } }; @@ -581,6 +767,7 @@ const projectId = Number.parseInt(projectSelect.value || "", 10); writeStoredId(storageKeys.projectId, Number.isInteger(projectId) && projectId > 0 ? projectId : null); writeStoredId(storageKeys.bookId, null); + resetPlotDirectorScene("Select a PlotDirector book."); await loadBooks(projectId, null); }); @@ -588,9 +775,11 @@ const bookId = Number.parseInt(bookSelect.value || "", 10); writeStoredId(storageKeys.bookId, Number.isInteger(bookId) && bookId > 0 ? bookId : null); updateSummary(); + resetPlotDirectorScene(selectedBook() ? "No scene detected in Word." : "Select a PlotDirector book."); }); refreshDocumentButton?.addEventListener("click", refreshDocumentStructure); + refreshPlotDirectorSceneButton?.addEventListener("click", refreshPlotDirectorScene); runDiagnosticsButton?.addEventListener("click", runDiagnostics); if (isAuthenticated) {
Select a PlotDirector book.