Phase 10J: Word Companion Scene Create / Link

This commit is contained in:
Nick Beckley 2026-06-14 09:02:24 +01:00
parent 120ee271f8
commit a9aae3fbf9
9 changed files with 613 additions and 6 deletions

View File

@ -22,7 +22,9 @@
<DefaultSettings>
<SourceLocation DefaultValue="https://localhost:7296/word-companion"/>
</DefaultSettings>
<Permissions>ReadWriteDocument</Permissions>
<!-- ReadDocument is required for the companion to read Word paragraphs, selection context,
and document body content. It does not grant document write access. -->
<Permissions>ReadDocument</Permissions>
<VersionOverrides xmlns="http://schemas.microsoft.com/office/taskpaneappversionoverrides" xsi:type="VersionOverridesV1_0">
<Hosts>
<Host xsi:type="Document">

View File

@ -46,6 +46,13 @@ public sealed class WordCompanionController(IWordCompanionService wordCompanion)
return response is null ? NotFound() : Ok(response);
}
[HttpPost("books/{bookId:int}/chapters/{chapterId:int}/scenes")]
public async Task<IActionResult> CreateScene(int bookId, int chapterId, WordCompanionCreateSceneRequest request)
{
var response = await wordCompanion.CreateSceneAsync(bookId, chapterId, request);
return response is null ? BadRequest() : Ok(response);
}
[HttpPost("books/{bookId:int}/sync-scene")]
public async Task<IActionResult> SyncScene(int bookId, WordCompanionSyncSceneRequest request)
{

View File

@ -11,6 +11,7 @@ public interface IWordCompanionRepository
Task<WordCompanionBookStructureResponse?> GetBookStructureAsync(int bookId, int userId);
Task<WordCompanionResolveSceneResponse> ResolveSceneAsync(int bookId, int userId, string chapterTitle, string sceneTitle);
Task<WordCompanionSceneCompanionResponse?> GetSceneCompanionAsync(int sceneId, int userId);
Task<WordCompanionCreateSceneResponse?> CreateSceneAsync(int bookId, int chapterId, int userId, WordCompanionCreateSceneRequest request);
Task<WordCompanionSyncSceneResponse?> SyncSceneAsync(int bookId, int userId, WordCompanionSyncSceneRequest request);
Task<WordCompanionSyncManuscriptResponse?> SyncManuscriptAsync(int bookId, int userId, WordCompanionSyncManuscriptRequest request);
Task<WordCompanionUpdateSceneLinksResponse?> UpdateSceneLinksAsync(int sceneId, int userId, WordCompanionUpdateSceneLinksRequest request);
@ -115,6 +116,15 @@ public sealed class WordCompanionRepository(ISqlConnectionFactory connectionFact
};
}
public async Task<WordCompanionCreateSceneResponse?> CreateSceneAsync(int bookId, int chapterId, int userId, WordCompanionCreateSceneRequest request)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleOrDefaultAsync<WordCompanionCreateSceneResponse>(
"dbo.WordCompanion_Scene_Create",
new { BookID = bookId, ChapterID = chapterId, UserID = userId, request.SceneTitle },
commandType: CommandType.StoredProcedure);
}
public async Task<WordCompanionSyncSceneResponse?> SyncSceneAsync(int bookId, int userId, WordCompanionSyncSceneRequest request)
{
using var connection = connectionFactory.CreateConnection();

View File

@ -114,6 +114,18 @@ public sealed class WordCompanionSyncSceneResponse
public string Message { get; init; } = "Scene synced successfully.";
}
public sealed class WordCompanionCreateSceneRequest
{
public string? SceneTitle { get; set; }
}
public sealed class WordCompanionCreateSceneResponse
{
public int ChapterId { get; init; }
public int SceneId { get; init; }
public string Message { get; init; } = "Scene created.";
}
public sealed class WordCompanionSyncManuscriptRequest
{
public string? DocumentId { get; set; }

View File

@ -10,6 +10,7 @@ public interface IWordCompanionService
Task<WordCompanionBookStructureResponse?> GetBookStructureAsync(int bookId);
Task<WordCompanionResolveSceneResponse?> ResolveSceneAsync(int bookId, WordCompanionResolveSceneRequest request);
Task<WordCompanionSceneCompanionResponse?> GetSceneCompanionAsync(int sceneId);
Task<WordCompanionCreateSceneResponse?> CreateSceneAsync(int bookId, int chapterId, WordCompanionCreateSceneRequest request);
Task<WordCompanionSyncSceneResponse?> SyncSceneAsync(int bookId, WordCompanionSyncSceneRequest request);
Task<WordCompanionSyncManuscriptResponse?> SyncManuscriptAsync(int bookId, WordCompanionSyncManuscriptRequest request);
Task<WordCompanionUpdateSceneLinksResponse?> UpdateSceneLinksAsync(int sceneId, WordCompanionUpdateSceneLinksRequest request);
@ -55,6 +56,17 @@ public sealed class WordCompanionService(IWordCompanionRepository repository, IC
return sceneId <= 0 ? Task.FromResult<WordCompanionSceneCompanionResponse?>(null) : repository.GetSceneCompanionAsync(sceneId, RequireUserId());
}
public Task<WordCompanionCreateSceneResponse?> CreateSceneAsync(int bookId, int chapterId, WordCompanionCreateSceneRequest request)
{
if (bookId <= 0 || chapterId <= 0 || string.IsNullOrWhiteSpace(request.SceneTitle))
{
return Task.FromResult<WordCompanionCreateSceneResponse?>(null);
}
request.SceneTitle = request.SceneTitle.Trim();
return repository.CreateSceneAsync(bookId, chapterId, RequireUserId(), request);
}
public Task<WordCompanionSyncSceneResponse?> SyncSceneAsync(int bookId, WordCompanionSyncSceneRequest request)
{
if (bookId <= 0

View File

@ -0,0 +1,85 @@
SET ANSI_NULLS ON;
GO
SET QUOTED_IDENTIFIER ON;
GO
CREATE OR ALTER PROCEDURE dbo.WordCompanion_Scene_Create
@BookID int,
@ChapterID int,
@UserID int,
@SceneTitle nvarchar(200)
AS
BEGIN
SET NOCOUNT ON;
DECLARE @TrimmedSceneTitle nvarchar(200) = LTRIM(RTRIM(COALESCE(@SceneTitle, N'')));
IF NULLIF(@TrimmedSceneTitle, N'') IS NULL
RETURN;
IF NOT EXISTS
(
SELECT 1
FROM dbo.Chapters c
INNER JOIN dbo.Books b ON b.BookID = c.BookID
INNER JOIN dbo.Projects p ON p.ProjectID = b.ProjectID
INNER JOIN dbo.ProjectUserAccess pua ON pua.ProjectID = p.ProjectID
WHERE b.BookID = @BookID
AND c.ChapterID = @ChapterID
AND c.IsArchived = 0
AND b.IsArchived = 0
AND p.IsArchived = 0
AND pua.UserID = @UserID
AND pua.IsActive = 1
)
RETURN;
DECLARE @PlannedRevisionStatusID int = (SELECT TOP (1) RevisionStatusID FROM dbo.RevisionStatuses WHERE StatusName = N'Planned' ORDER BY SortOrder);
DECLARE @UnknownTimeModeID int = (SELECT TOP (1) TimeModeID FROM dbo.TimeModes WHERE TimeModeName = N'Unknown' ORDER BY SortOrder);
DECLARE @UnknownTimeConfidenceID int = (SELECT TOP (1) TimeConfidenceID FROM dbo.TimeConfidences WHERE TimeConfidenceName = N'Unknown' ORDER BY SortOrder);
IF @PlannedRevisionStatusID IS NULL OR @UnknownTimeModeID IS NULL OR @UnknownTimeConfidenceID IS NULL
RETURN;
DECLARE @NextSceneNumber decimal(9,2) = ISNULL((SELECT MAX(SceneNumber) FROM dbo.Scenes WHERE ChapterID = @ChapterID), 0) + 1;
DECLARE @NextSceneOrder int = ISNULL((SELECT MAX(SortOrder) FROM dbo.Scenes WHERE ChapterID = @ChapterID), 0) + 10;
DECLARE @SceneID int;
INSERT dbo.Scenes
(
ChapterID,
SceneNumber,
SceneTitle,
Summary,
SortOrder,
TimeModeID,
TimeConfidenceID,
ScenePurposeNotes,
SceneOutcomeNotes,
RevisionStatusID,
PrimaryLocationID
)
VALUES
(
@ChapterID,
@NextSceneNumber,
@TrimmedSceneTitle,
NULL,
@NextSceneOrder,
@UnknownTimeModeID,
@UnknownTimeConfidenceID,
NULL,
NULL,
@PlannedRevisionStatusID,
NULL
);
SET @SceneID = CAST(SCOPE_IDENTITY() AS int);
INSERT dbo.SceneWorkflow (SceneID, DraftStatus, EstimatedWordCount, ActualWordCount, IsBlocked)
VALUES (@SceneID, N'Planned', 0, 0, 0);
SELECT @ChapterID AS ChapterId,
@SceneID AS SceneId,
N'Scene created.' AS Message;
END;
GO

View File

@ -121,6 +121,27 @@
<p class="word-companion-message" data-sync-note></p>
</section>
<section class="word-companion-section word-companion-create-link" data-scene-create-link hidden aria-label="Scene Create Link">
<div class="word-companion-section-header">
<span>Scene Create / Link</span>
</div>
<div class="word-companion-scene-card">
<div>
<span>Current Chapter</span>
<strong data-create-link-chapter>-</strong>
</div>
<div>
<span>Current Scene</span>
<strong data-create-link-scene>-</strong>
</div>
</div>
<div class="word-companion-actions">
<button type="button" data-create-plotdirector-scene disabled>Create Scene in PlotDirector</button>
<button type="button" data-link-existing-scene disabled>Link to Existing Scene</button>
</div>
<p class="word-companion-message" data-create-link-status></p>
</section>
<section class="word-companion-section word-companion-brief" data-writing-brief-block hidden aria-label="Writing Brief">
<span>Writing Brief</span>
<p data-writing-brief></p>
@ -264,6 +285,36 @@
<footer class="word-companion-footer">
<span data-office-status>Word Companion loading</span>
</footer>
<dialog class="word-companion-dialog" data-link-existing-dialog>
<form method="dialog">
<div class="word-companion-section-header">
<span>Link to Existing Scene</span>
<button type="button" data-link-dialog-cancel>Cancel</button>
</div>
<label for="word-companion-scene-search">Search scenes</label>
<input id="word-companion-scene-search" type="search" data-link-scene-search autocomplete="off" />
<div class="word-companion-scene-options" data-link-scene-options></div>
<button type="button" data-link-selected-scene disabled>Link Scene</button>
<p class="word-companion-message" data-link-dialog-status></p>
</form>
</dialog>
<dialog class="word-companion-dialog" data-create-scene-dialog>
<form method="dialog">
<div class="word-companion-section-header">
<span>Create PlotDirector Scene</span>
</div>
<p class="word-companion-dialog-copy">Create PlotDirector scene:</p>
<strong data-create-dialog-scene>-</strong>
<p class="word-companion-dialog-copy">within chapter:</p>
<strong data-create-dialog-chapter>-</strong>
<div class="word-companion-dialog-actions">
<button type="button" data-create-dialog-confirm>Create</button>
<button type="button" data-create-dialog-cancel>Cancel</button>
</div>
</form>
</dialog>
</main>
<script src="~/js/word-companion-host.js" asp-append-version="true"></script>
</body>

View File

@ -339,6 +339,103 @@ body {
color: var(--companion-muted);
}
.word-companion-create-link {
border-color: rgba(47, 111, 99, 0.38);
}
.word-companion-dialog {
width: min(360px, calc(100vw - 24px));
border: 1px solid var(--companion-line);
border-radius: 8px;
padding: 12px;
background: var(--companion-panel);
color: var(--companion-ink);
font-family: "Segoe UI", system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
}
.word-companion-dialog::backdrop {
background: rgba(30, 37, 43, 0.28);
}
.word-companion-dialog form {
display: grid;
gap: 8px;
}
.word-companion-dialog strong {
min-width: 0;
overflow-wrap: anywhere;
}
.word-companion-dialog-copy {
margin: 0;
color: var(--companion-muted);
font-size: 0.9rem;
}
.word-companion-dialog-actions {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
}
.word-companion-dialog input[type="search"] {
width: 100%;
min-height: 36px;
border: 1px solid var(--companion-line);
border-radius: 6px;
padding: 6px 8px;
background: #ffffff;
color: var(--companion-ink);
font: inherit;
}
.word-companion-dialog button {
min-height: 32px;
border: 1px solid var(--companion-accent);
border-radius: 6px;
padding: 5px 9px;
background: var(--companion-accent);
color: #ffffff;
font: inherit;
font-size: 0.82rem;
font-weight: 700;
}
.word-companion-dialog button:disabled {
border-color: var(--companion-line);
background: var(--companion-soft);
color: var(--companion-muted);
}
.word-companion-scene-options {
display: grid;
gap: 5px;
max-height: 220px;
overflow-y: auto;
}
.word-companion-scene-option {
display: grid;
grid-template-columns: auto 1fr;
align-items: center;
gap: 7px;
min-height: 32px;
border: 1px solid rgba(47, 111, 99, 0.18);
border-radius: 6px;
padding: 5px 7px;
background: #ffffff;
color: var(--companion-ink);
font-size: 0.9rem;
}
.word-companion-scene-option input {
width: 16px;
height: 16px;
margin: 0;
accent-color: var(--companion-accent);
}
.word-companion-empty-list {
margin: 0;
color: var(--companion-muted);

View File

@ -51,6 +51,23 @@
const primaryLocationSelect = document.querySelector("[data-primary-location-select]");
const saveSceneLinksButton = document.querySelector("[data-save-scene-links]");
const sceneLinksStatus = document.querySelector("[data-scene-links-status]");
const sceneCreateLink = document.querySelector("[data-scene-create-link]");
const createLinkChapter = document.querySelector("[data-create-link-chapter]");
const createLinkScene = document.querySelector("[data-create-link-scene]");
const createPlotDirectorSceneButton = document.querySelector("[data-create-plotdirector-scene]");
const linkExistingSceneButton = document.querySelector("[data-link-existing-scene]");
const createLinkStatus = document.querySelector("[data-create-link-status]");
const linkExistingDialog = document.querySelector("[data-link-existing-dialog]");
const linkSceneSearch = document.querySelector("[data-link-scene-search]");
const linkSceneOptions = document.querySelector("[data-link-scene-options]");
const linkSelectedSceneButton = document.querySelector("[data-link-selected-scene]");
const linkDialogCancel = document.querySelector("[data-link-dialog-cancel]");
const linkDialogStatus = document.querySelector("[data-link-dialog-status]");
const createSceneDialog = document.querySelector("[data-create-scene-dialog]");
const createDialogScene = document.querySelector("[data-create-dialog-scene]");
const createDialogChapter = document.querySelector("[data-create-dialog-chapter]");
const createDialogConfirm = document.querySelector("[data-create-dialog-confirm]");
const createDialogCancel = document.querySelector("[data-create-dialog-cancel]");
const resolveProjectId = document.querySelector("[data-resolve-project-id]");
const resolveProjectTitle = document.querySelector("[data-resolve-project-title]");
const resolveBookId = document.querySelector("[data-resolve-book-id]");
@ -93,6 +110,9 @@
let currentResolutionMethod = "";
let currentResolutionAnchorType = "Title Match";
let storedAnchorInvalid = false;
let linkDialogScenes = [];
let selectedLinkSceneId = null;
let pendingCreateSceneConfirmation = null;
let currentSceneLinks = {
characters: [],
assets: [],
@ -244,11 +264,48 @@
setText(sceneLinksStatus, message);
};
const setCreateLinkStatus = (message) => {
setText(createLinkStatus, message);
};
const canUseCreateLinkActions = () => !!selectedBook() && !!detectedSceneTitle;
const hideSceneCreateLink = () => {
setHidden(sceneCreateLink, true);
setCreateLinkStatus("");
if (createPlotDirectorSceneButton) {
createPlotDirectorSceneButton.disabled = true;
createPlotDirectorSceneButton.textContent = "Create Scene in PlotDirector";
}
if (linkExistingSceneButton) {
linkExistingSceneButton.disabled = true;
linkExistingSceneButton.textContent = "Link to Existing Scene";
}
};
const showSceneCreateLink = (message = "") => {
setText(createLinkChapter, displayValue(detectedChapterTitle));
setText(createLinkScene, displayValue(detectedSceneTitle));
setHidden(sceneCreateLink, false);
setCreateLinkStatus(message);
const enabled = canUseCreateLinkActions();
if (createPlotDirectorSceneButton) {
createPlotDirectorSceneButton.disabled = !enabled;
createPlotDirectorSceneButton.textContent = "Create Scene in PlotDirector";
}
if (linkExistingSceneButton) {
linkExistingSceneButton.disabled = !enabled;
linkExistingSceneButton.textContent = "Link to Existing Scene";
}
};
const resetPlotDirectorScene = (message) => {
setPlotDirectorSceneStatus(message);
setHidden(plotDirectorSceneDetails, true);
setHidden(writingBriefBlock, true);
setHidden(plotDirectorLinkGroups, true);
hideSceneCreateLink();
currentResolutionMethod = "";
currentResolutionAnchorType = detectedSceneAnchorId ? "SceneID Anchor" : detectedChapterAnchorId ? "ChapterID Anchor" : "Title Match";
storedAnchorInvalid = false;
@ -441,6 +498,30 @@
return null;
};
const getBookStructureChapters = async (bookId) => {
const structure = await fetchJson(`/api/word-companion/books/${bookId}/structure`);
return Array.isArray(structure?.chapters) ? structure.chapters : [];
};
const findCurrentChapterInChapters = (chapters) => {
const requestChapterTitle = normalizeTitleForResolve(detectedChapterTitle, "chapter").toLocaleLowerCase();
if (!requestChapterTitle) {
return null;
}
return chapters.find((chapter) =>
normalizeTitleForResolve(chapter.title, "chapter").toLocaleLowerCase() === requestChapterTitle) || null;
};
const findCurrentChapter = async () => {
const book = selectedBook();
if (!book) {
return null;
}
return findCurrentChapterInChapters(await getBookStructureChapters(book.bookId));
};
const loadResolvedCompanionData = async (bookId, sceneId, chapterId, resolutionMethod, anchorType) => {
const companionData = await fetchJson(`/api/word-companion/scenes/${sceneId}/companion`);
try {
@ -452,6 +533,7 @@
storedAnchorInvalid = false;
setPlotDirectorSceneStatus("Linked to PlotDirector");
setDocumentMessage("");
hideSceneCreateLink();
setResolvedScene(sceneId, chapterId, resolutionMethod);
renderCompanionData(companionData);
};
@ -1006,21 +1088,21 @@
return control;
};
const attachPlotDirectorIds = async () => {
const attachResolvedPlotDirectorIds = async (successMessage = "PlotDirector IDs attached successfully.") => {
if (!resolvedSceneId || !resolvedChapterId) {
setDocumentMessage("No resolved PlotDirector scene.");
return;
return false;
}
const replacingExistingAnchor = (detectedSceneAnchorId && detectedSceneAnchorId !== resolvedSceneId)
|| (detectedChapterAnchorId && detectedChapterAnchorId !== resolvedChapterId);
if (replacingExistingAnchor && !window.confirm("This scene is already linked. Replace the existing PlotDirector link?")) {
return;
return false;
}
if (!officeReady || !wordHostAvailable || !window.Word || typeof window.Word.run !== "function") {
setDocumentMessage("Word document access is unavailable.");
return;
return false;
}
if (attachPlotDirectorIdsButton) {
@ -1051,14 +1133,16 @@
storedAnchorInvalid = false;
currentResolutionAnchorType = "SceneID Anchor";
currentResolutionMethod = "Anchor";
setDocumentMessage("PlotDirector IDs attached successfully.");
setDocumentMessage(successMessage);
setDiagnostics({ lastError: "-" });
updateAnchorDisplay();
return true;
} catch (error) {
console.error("Unable to attach PlotDirector IDs.", error);
setDocumentMessage("Unable to attach PlotDirector IDs.");
setDiagnostics({ lastError: errorText(error) });
updateAnchorDisplay();
return false;
} finally {
if (attachPlotDirectorIdsButton) {
attachPlotDirectorIdsButton.textContent = detectedSceneAnchorId === resolvedSceneId
@ -1069,6 +1153,239 @@
}
};
const attachPlotDirectorIds = async () => {
await attachResolvedPlotDirectorIds();
};
const resolveCreatedOrLinkedScene = async (sceneId, chapterId, successMessage) => {
const book = selectedBook();
if (!book) {
setCreateLinkStatus("Select a PlotDirector book first.");
return false;
}
await loadResolvedCompanionData(book.bookId, sceneId, chapterId, "Manual link", "Title Match");
const anchored = await attachResolvedPlotDirectorIds(successMessage);
if (!anchored) {
return false;
}
setCreateLinkStatus(successMessage);
hideSceneCreateLink();
return true;
};
const closeCreateSceneDialog = (confirmed) => {
if (pendingCreateSceneConfirmation) {
pendingCreateSceneConfirmation(confirmed);
pendingCreateSceneConfirmation = null;
}
if (createSceneDialog?.close) {
createSceneDialog.close();
} else if (createSceneDialog) {
createSceneDialog.hidden = true;
}
};
const confirmCreateScene = (sceneTitle, chapterTitle) => {
setText(createDialogScene, `"${sceneTitle}"`);
setText(createDialogChapter, `"${chapterTitle}"`);
if (!createSceneDialog) {
return Promise.resolve(window.confirm(`Create PlotDirector scene:\n\n"${sceneTitle}"\n\nwithin chapter:\n\n"${chapterTitle}"\n\n?`));
}
return new Promise((resolve) => {
pendingCreateSceneConfirmation = resolve;
if (createSceneDialog.showModal) {
createSceneDialog.showModal();
} else {
createSceneDialog.hidden = false;
}
});
};
const createPlotDirectorScene = async () => {
const book = selectedBook();
if (!book || !detectedSceneTitle) {
showSceneCreateLink("Scene not found in PlotDirector.");
return;
}
const sceneTitle = normalizeTitleForResolve(detectedSceneTitle, "scene");
const chapterTitle = normalizeTitleForResolve(detectedChapterTitle, "chapter");
const confirmed = await confirmCreateScene(sceneTitle, chapterTitle);
if (!confirmed) {
return;
}
if (createPlotDirectorSceneButton) {
createPlotDirectorSceneButton.disabled = true;
createPlotDirectorSceneButton.textContent = "Creating...";
}
try {
const chapter = await findCurrentChapter();
if (!chapter) {
showSceneCreateLink("This chapter does not exist in PlotDirector. Create or link the chapter first.");
return;
}
const response = await postJson(`/api/word-companion/books/${book.bookId}/chapters/${chapter.chapterId}/scenes`, {
sceneTitle
});
if (!response?.sceneId || !response?.chapterId) {
throw new Error("Scene creation did not return a scene ID.");
}
await resolveCreatedOrLinkedScene(response.sceneId, response.chapterId, "Scene created and linked successfully.");
} catch (error) {
setCreateLinkStatus("Unable to create scene.");
setDiagnostics({ lastError: errorText(error) });
} finally {
if (createPlotDirectorSceneButton) {
createPlotDirectorSceneButton.disabled = !canUseCreateLinkActions();
createPlotDirectorSceneButton.textContent = "Create Scene in PlotDirector";
}
}
};
const renderLinkSceneOptions = () => {
if (!linkSceneOptions) {
return;
}
const query = normalizeWhitespace(linkSceneSearch?.value || "").toLocaleLowerCase();
const rows = linkDialogScenes.filter((scene) => !query || String(scene.title || "").toLocaleLowerCase().includes(query));
linkSceneOptions.innerHTML = "";
selectedLinkSceneId = null;
if (linkSelectedSceneButton) {
linkSelectedSceneButton.disabled = true;
}
if (rows.length === 0) {
const empty = document.createElement("p");
empty.className = "word-companion-empty-list";
empty.textContent = "No scenes found.";
linkSceneOptions.append(empty);
return;
}
for (const scene of rows) {
const sceneId = Number.parseInt(scene.sceneId || "", 10);
if (!Number.isInteger(sceneId) || sceneId <= 0) {
continue;
}
const label = document.createElement("label");
label.className = "word-companion-scene-option";
const radio = document.createElement("input");
radio.type = "radio";
radio.name = "word-companion-link-scene";
radio.value = String(sceneId);
radio.addEventListener("change", () => {
selectedLinkSceneId = sceneId;
if (linkSelectedSceneButton) {
linkSelectedSceneButton.disabled = false;
}
});
const text = document.createElement("span");
text.textContent = scene.title || "Untitled scene";
label.append(radio, text);
linkSceneOptions.append(label);
}
};
const openLinkExistingSceneDialog = async () => {
const book = selectedBook();
if (!book || !detectedSceneTitle) {
showSceneCreateLink("Scene not found in PlotDirector.");
return;
}
if (linkExistingSceneButton) {
linkExistingSceneButton.disabled = true;
linkExistingSceneButton.textContent = "Loading...";
}
try {
const chapters = await getBookStructureChapters(book.bookId);
const chapter = findCurrentChapterInChapters(chapters);
if (!chapter) {
showSceneCreateLink("This chapter does not exist in PlotDirector. Create or link the chapter first.");
return;
}
linkDialogScenes = Array.isArray(chapter.scenes) ? chapter.scenes : [];
selectedLinkSceneId = null;
if (linkSceneSearch) {
linkSceneSearch.value = "";
}
setText(linkDialogStatus, "");
renderLinkSceneOptions();
if (linkExistingDialog?.showModal) {
linkExistingDialog.showModal();
} else if (linkExistingDialog) {
linkExistingDialog.hidden = false;
}
} catch (error) {
setCreateLinkStatus("Unable to load scenes.");
setDiagnostics({ lastError: errorText(error) });
} finally {
if (linkExistingSceneButton) {
linkExistingSceneButton.disabled = !canUseCreateLinkActions();
linkExistingSceneButton.textContent = "Link to Existing Scene";
}
}
};
const closeLinkExistingSceneDialog = () => {
if (linkExistingDialog?.close) {
linkExistingDialog.close();
} else if (linkExistingDialog) {
linkExistingDialog.hidden = true;
}
};
const linkSelectedExistingScene = async () => {
const book = selectedBook();
const scene = linkDialogScenes.find((item) => item.sceneId === selectedLinkSceneId);
if (!book || !scene) {
setText(linkDialogStatus, "Select a scene.");
return;
}
if (linkSelectedSceneButton) {
linkSelectedSceneButton.disabled = true;
linkSelectedSceneButton.textContent = "Linking...";
}
try {
const chapter = await findCurrentChapter();
if (!chapter) {
throw new Error("This chapter does not exist in PlotDirector. Create or link the chapter first.");
}
closeLinkExistingSceneDialog();
await resolveCreatedOrLinkedScene(scene.sceneId, chapter.chapterId, "Scene linked successfully.");
} catch (error) {
setText(linkDialogStatus, "Unable to link scene.");
setCreateLinkStatus("Unable to link scene.");
setDiagnostics({ lastError: errorText(error) });
} finally {
if (linkSelectedSceneButton) {
linkSelectedSceneButton.disabled = !selectedLinkSceneId;
linkSelectedSceneButton.textContent = "Link Scene";
}
}
};
const syncSceneProgress = async () => {
if (!resolvedSceneId) {
setSyncStatus("No resolved PlotDirector scene.");
@ -1237,6 +1554,9 @@
chapterId: resolveResponse?.chapterId,
sceneId: resolveResponse?.sceneId
});
if (!hadInvalidAnchor) {
showSceneCreateLink("Scene not found in PlotDirector.");
}
restorePlotDirectorSceneButton();
return false;
}
@ -1406,6 +1726,17 @@
syncSceneProgressButton?.addEventListener("click", syncSceneProgress);
saveSceneLinksButton?.addEventListener("click", saveSceneLinks);
attachPlotDirectorIdsButton?.addEventListener("click", attachPlotDirectorIds);
createPlotDirectorSceneButton?.addEventListener("click", createPlotDirectorScene);
linkExistingSceneButton?.addEventListener("click", openLinkExistingSceneDialog);
linkSceneSearch?.addEventListener("input", renderLinkSceneOptions);
linkSelectedSceneButton?.addEventListener("click", linkSelectedExistingScene);
linkDialogCancel?.addEventListener("click", closeLinkExistingSceneDialog);
createDialogConfirm?.addEventListener("click", () => closeCreateSceneDialog(true));
createDialogCancel?.addEventListener("click", () => closeCreateSceneDialog(false));
createSceneDialog?.addEventListener("cancel", (event) => {
event.preventDefault();
closeCreateSceneDialog(false);
});
runDiagnosticsButton?.addEventListener("click", runDiagnostics);
if (isAuthenticated) {