Phase 10J: Word Companion Scene Create / Link
This commit is contained in:
parent
120ee271f8
commit
a9aae3fbf9
@ -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">
|
||||
|
||||
@ -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)
|
||||
{
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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; }
|
||||
|
||||
@ -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
|
||||
|
||||
85
PlotLine/Sql/062_Phase10J_WordCompanionSceneCreateLink.sql
Normal file
85
PlotLine/Sql/062_Phase10J_WordCompanionSceneCreateLink.sql
Normal 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
|
||||
@ -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>
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user