Phase 10K chapter create/link foundation.
This commit is contained in:
parent
867d536f82
commit
ffcbe99028
@ -27,6 +27,13 @@ public sealed class WordCompanionController(IWordCompanionService wordCompanion)
|
|||||||
return response is null ? NotFound() : Ok(response);
|
return response is null ? NotFound() : Ok(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet("books/{bookId:int}/chapters")]
|
||||||
|
public async Task<IActionResult> GetChapters(int bookId)
|
||||||
|
{
|
||||||
|
var response = await wordCompanion.ListChaptersAsync(bookId);
|
||||||
|
return response is null ? BadRequest() : Ok(response);
|
||||||
|
}
|
||||||
|
|
||||||
[HttpPost("books/{bookId:int}/resolve-scene")]
|
[HttpPost("books/{bookId:int}/resolve-scene")]
|
||||||
public async Task<IActionResult> ResolveScene(int bookId, WordCompanionResolveSceneRequest request)
|
public async Task<IActionResult> ResolveScene(int bookId, WordCompanionResolveSceneRequest request)
|
||||||
{
|
{
|
||||||
@ -46,6 +53,13 @@ public sealed class WordCompanionController(IWordCompanionService wordCompanion)
|
|||||||
return response is null ? NotFound() : Ok(response);
|
return response is null ? NotFound() : Ok(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpPost("books/{bookId:int}/chapters")]
|
||||||
|
public async Task<IActionResult> CreateChapter(int bookId, WordCompanionCreateChapterRequest request)
|
||||||
|
{
|
||||||
|
var response = await wordCompanion.CreateChapterAsync(bookId, request);
|
||||||
|
return response is null ? BadRequest() : Ok(response);
|
||||||
|
}
|
||||||
|
|
||||||
[HttpPost("books/{bookId:int}/chapters/{chapterId:int}/scenes")]
|
[HttpPost("books/{bookId:int}/chapters/{chapterId:int}/scenes")]
|
||||||
public async Task<IActionResult> CreateScene(int bookId, int chapterId, WordCompanionCreateSceneRequest request)
|
public async Task<IActionResult> CreateScene(int bookId, int chapterId, WordCompanionCreateSceneRequest request)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -9,8 +9,10 @@ public interface IWordCompanionRepository
|
|||||||
Task<IReadOnlyList<WordCompanionProjectDto>> ListProjectsAsync(int userId);
|
Task<IReadOnlyList<WordCompanionProjectDto>> ListProjectsAsync(int userId);
|
||||||
Task<IReadOnlyList<WordCompanionBookDto>> ListBooksAsync(int projectId, int userId);
|
Task<IReadOnlyList<WordCompanionBookDto>> ListBooksAsync(int projectId, int userId);
|
||||||
Task<WordCompanionBookStructureResponse?> GetBookStructureAsync(int bookId, int userId);
|
Task<WordCompanionBookStructureResponse?> GetBookStructureAsync(int bookId, int userId);
|
||||||
|
Task<IReadOnlyList<WordCompanionChapterDto>> ListChaptersAsync(int bookId, int userId);
|
||||||
Task<WordCompanionResolveSceneResponse> ResolveSceneAsync(int bookId, int userId, string chapterTitle, string sceneTitle);
|
Task<WordCompanionResolveSceneResponse> ResolveSceneAsync(int bookId, int userId, string chapterTitle, string sceneTitle);
|
||||||
Task<WordCompanionSceneCompanionResponse?> GetSceneCompanionAsync(int sceneId, int userId);
|
Task<WordCompanionSceneCompanionResponse?> GetSceneCompanionAsync(int sceneId, int userId);
|
||||||
|
Task<WordCompanionCreateChapterResponse?> CreateChapterAsync(int bookId, int userId, WordCompanionCreateChapterRequest request);
|
||||||
Task<WordCompanionCreateSceneResponse?> CreateSceneAsync(int bookId, int chapterId, int userId, WordCompanionCreateSceneRequest request);
|
Task<WordCompanionCreateSceneResponse?> CreateSceneAsync(int bookId, int chapterId, int userId, WordCompanionCreateSceneRequest request);
|
||||||
Task<WordCompanionSyncSceneResponse?> SyncSceneAsync(int bookId, int userId, WordCompanionSyncSceneRequest request);
|
Task<WordCompanionSyncSceneResponse?> SyncSceneAsync(int bookId, int userId, WordCompanionSyncSceneRequest request);
|
||||||
Task<WordCompanionSyncManuscriptResponse?> SyncManuscriptAsync(int bookId, int userId, WordCompanionSyncManuscriptRequest request);
|
Task<WordCompanionSyncManuscriptResponse?> SyncManuscriptAsync(int bookId, int userId, WordCompanionSyncManuscriptRequest request);
|
||||||
@ -74,6 +76,16 @@ public sealed class WordCompanionRepository(ISqlConnectionFactory connectionFact
|
|||||||
return new WordCompanionBookStructureResponse { BookId = header.BookId, Chapters = chapters };
|
return new WordCompanionBookStructureResponse { BookId = header.BookId, Chapters = chapters };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<WordCompanionChapterDto>> ListChaptersAsync(int bookId, int userId)
|
||||||
|
{
|
||||||
|
using var connection = connectionFactory.CreateConnection();
|
||||||
|
var rows = await connection.QueryAsync<WordCompanionChapterDto>(
|
||||||
|
"dbo.WordCompanion_Chapter_List",
|
||||||
|
new { BookID = bookId, UserID = userId },
|
||||||
|
commandType: CommandType.StoredProcedure);
|
||||||
|
return rows.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<WordCompanionResolveSceneResponse> ResolveSceneAsync(int bookId, int userId, string chapterTitle, string sceneTitle)
|
public async Task<WordCompanionResolveSceneResponse> ResolveSceneAsync(int bookId, int userId, string chapterTitle, string sceneTitle)
|
||||||
{
|
{
|
||||||
using var connection = connectionFactory.CreateConnection();
|
using var connection = connectionFactory.CreateConnection();
|
||||||
@ -116,6 +128,15 @@ public sealed class WordCompanionRepository(ISqlConnectionFactory connectionFact
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<WordCompanionCreateChapterResponse?> CreateChapterAsync(int bookId, int userId, WordCompanionCreateChapterRequest request)
|
||||||
|
{
|
||||||
|
using var connection = connectionFactory.CreateConnection();
|
||||||
|
return await connection.QuerySingleOrDefaultAsync<WordCompanionCreateChapterResponse>(
|
||||||
|
"dbo.WordCompanion_Chapter_Create",
|
||||||
|
new { BookID = bookId, UserID = userId, request.Title, request.SortOrder },
|
||||||
|
commandType: CommandType.StoredProcedure);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<WordCompanionCreateSceneResponse?> CreateSceneAsync(int bookId, int chapterId, int userId, WordCompanionCreateSceneRequest request)
|
public async Task<WordCompanionCreateSceneResponse?> CreateSceneAsync(int bookId, int chapterId, int userId, WordCompanionCreateSceneRequest request)
|
||||||
{
|
{
|
||||||
using var connection = connectionFactory.CreateConnection();
|
using var connection = connectionFactory.CreateConnection();
|
||||||
|
|||||||
@ -47,6 +47,18 @@ public sealed class WordCompanionBookStructureResponse
|
|||||||
public IReadOnlyList<WordCompanionChapterStructureDto> Chapters { get; init; } = [];
|
public IReadOnlyList<WordCompanionChapterStructureDto> Chapters { get; init; } = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public sealed class WordCompanionChaptersResponse
|
||||||
|
{
|
||||||
|
public IReadOnlyList<WordCompanionChapterDto> Chapters { get; init; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class WordCompanionChapterDto
|
||||||
|
{
|
||||||
|
public int ChapterId { get; init; }
|
||||||
|
public string Title { get; init; } = string.Empty;
|
||||||
|
public int SortOrder { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
public sealed class WordCompanionResolveSceneRequest
|
public sealed class WordCompanionResolveSceneRequest
|
||||||
{
|
{
|
||||||
public string? ChapterTitle { get; set; }
|
public string? ChapterTitle { get; set; }
|
||||||
@ -127,6 +139,20 @@ public sealed class WordCompanionCreateSceneResponse
|
|||||||
public string Message { get; init; } = "Scene created.";
|
public string Message { get; init; } = "Scene created.";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public sealed class WordCompanionCreateChapterRequest
|
||||||
|
{
|
||||||
|
public string? Title { get; set; }
|
||||||
|
public int? SortOrder { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class WordCompanionCreateChapterResponse
|
||||||
|
{
|
||||||
|
public int ChapterId { get; init; }
|
||||||
|
public string Title { get; init; } = string.Empty;
|
||||||
|
public int SortOrder { get; init; }
|
||||||
|
public string Message { get; init; } = "Chapter created successfully.";
|
||||||
|
}
|
||||||
|
|
||||||
public sealed class WordCompanionSyncManuscriptRequest
|
public sealed class WordCompanionSyncManuscriptRequest
|
||||||
{
|
{
|
||||||
public string? DocumentId { get; set; }
|
public string? DocumentId { get; set; }
|
||||||
|
|||||||
@ -8,8 +8,10 @@ public interface IWordCompanionService
|
|||||||
Task<WordCompanionProjectsResponse> ListProjectsAsync();
|
Task<WordCompanionProjectsResponse> ListProjectsAsync();
|
||||||
Task<WordCompanionBooksResponse?> ListBooksAsync(int projectId);
|
Task<WordCompanionBooksResponse?> ListBooksAsync(int projectId);
|
||||||
Task<WordCompanionBookStructureResponse?> GetBookStructureAsync(int bookId);
|
Task<WordCompanionBookStructureResponse?> GetBookStructureAsync(int bookId);
|
||||||
|
Task<WordCompanionChaptersResponse?> ListChaptersAsync(int bookId);
|
||||||
Task<WordCompanionResolveSceneResponse?> ResolveSceneAsync(int bookId, WordCompanionResolveSceneRequest request);
|
Task<WordCompanionResolveSceneResponse?> ResolveSceneAsync(int bookId, WordCompanionResolveSceneRequest request);
|
||||||
Task<WordCompanionSceneCompanionResponse?> GetSceneCompanionAsync(int sceneId);
|
Task<WordCompanionSceneCompanionResponse?> GetSceneCompanionAsync(int sceneId);
|
||||||
|
Task<WordCompanionCreateChapterResponse?> CreateChapterAsync(int bookId, WordCompanionCreateChapterRequest request);
|
||||||
Task<WordCompanionCreateSceneResponse?> CreateSceneAsync(int bookId, int chapterId, WordCompanionCreateSceneRequest request);
|
Task<WordCompanionCreateSceneResponse?> CreateSceneAsync(int bookId, int chapterId, WordCompanionCreateSceneRequest request);
|
||||||
Task<WordCompanionSyncSceneResponse?> SyncSceneAsync(int bookId, WordCompanionSyncSceneRequest request);
|
Task<WordCompanionSyncSceneResponse?> SyncSceneAsync(int bookId, WordCompanionSyncSceneRequest request);
|
||||||
Task<WordCompanionSyncManuscriptResponse?> SyncManuscriptAsync(int bookId, WordCompanionSyncManuscriptRequest request);
|
Task<WordCompanionSyncManuscriptResponse?> SyncManuscriptAsync(int bookId, WordCompanionSyncManuscriptRequest request);
|
||||||
@ -41,6 +43,17 @@ public sealed class WordCompanionService(IWordCompanionRepository repository, IC
|
|||||||
return bookId <= 0 ? Task.FromResult<WordCompanionBookStructureResponse?>(null) : repository.GetBookStructureAsync(bookId, RequireUserId());
|
return bookId <= 0 ? Task.FromResult<WordCompanionBookStructureResponse?>(null) : repository.GetBookStructureAsync(bookId, RequireUserId());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<WordCompanionChaptersResponse?> ListChaptersAsync(int bookId)
|
||||||
|
{
|
||||||
|
if (bookId <= 0)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var chapters = await repository.ListChaptersAsync(bookId, RequireUserId());
|
||||||
|
return new WordCompanionChaptersResponse { Chapters = chapters };
|
||||||
|
}
|
||||||
|
|
||||||
public Task<WordCompanionResolveSceneResponse?> ResolveSceneAsync(int bookId, WordCompanionResolveSceneRequest request)
|
public Task<WordCompanionResolveSceneResponse?> ResolveSceneAsync(int bookId, WordCompanionResolveSceneRequest request)
|
||||||
{
|
{
|
||||||
if (bookId <= 0 || string.IsNullOrWhiteSpace(request.ChapterTitle) || string.IsNullOrWhiteSpace(request.SceneTitle))
|
if (bookId <= 0 || string.IsNullOrWhiteSpace(request.ChapterTitle) || string.IsNullOrWhiteSpace(request.SceneTitle))
|
||||||
@ -56,6 +69,17 @@ public sealed class WordCompanionService(IWordCompanionRepository repository, IC
|
|||||||
return sceneId <= 0 ? Task.FromResult<WordCompanionSceneCompanionResponse?>(null) : repository.GetSceneCompanionAsync(sceneId, RequireUserId());
|
return sceneId <= 0 ? Task.FromResult<WordCompanionSceneCompanionResponse?>(null) : repository.GetSceneCompanionAsync(sceneId, RequireUserId());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Task<WordCompanionCreateChapterResponse?> CreateChapterAsync(int bookId, WordCompanionCreateChapterRequest request)
|
||||||
|
{
|
||||||
|
if (bookId <= 0 || string.IsNullOrWhiteSpace(request.Title) || request.SortOrder is <= 0)
|
||||||
|
{
|
||||||
|
return Task.FromResult<WordCompanionCreateChapterResponse?>(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
request.Title = request.Title.Trim();
|
||||||
|
return repository.CreateChapterAsync(bookId, RequireUserId(), request);
|
||||||
|
}
|
||||||
|
|
||||||
public Task<WordCompanionCreateSceneResponse?> CreateSceneAsync(int bookId, int chapterId, WordCompanionCreateSceneRequest request)
|
public Task<WordCompanionCreateSceneResponse?> CreateSceneAsync(int bookId, int chapterId, WordCompanionCreateSceneRequest request)
|
||||||
{
|
{
|
||||||
if (bookId <= 0 || chapterId <= 0 || string.IsNullOrWhiteSpace(request.SceneTitle))
|
if (bookId <= 0 || chapterId <= 0 || string.IsNullOrWhiteSpace(request.SceneTitle))
|
||||||
|
|||||||
82
PlotLine/Sql/064_Phase10K_WordCompanionChapterCreateLink.sql
Normal file
82
PlotLine/Sql/064_Phase10K_WordCompanionChapterCreateLink.sql
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
SET ANSI_NULLS ON;
|
||||||
|
GO
|
||||||
|
SET QUOTED_IDENTIFIER ON;
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE OR ALTER PROCEDURE dbo.WordCompanion_Chapter_List
|
||||||
|
@BookID int,
|
||||||
|
@UserID int
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
|
||||||
|
IF NOT EXISTS
|
||||||
|
(
|
||||||
|
SELECT 1
|
||||||
|
FROM dbo.Books b
|
||||||
|
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 b.IsArchived = 0
|
||||||
|
AND p.IsArchived = 0
|
||||||
|
AND pua.UserID = @UserID
|
||||||
|
AND pua.IsActive = 1
|
||||||
|
)
|
||||||
|
RETURN;
|
||||||
|
|
||||||
|
SELECT c.ChapterID AS ChapterId,
|
||||||
|
c.ChapterTitle AS Title,
|
||||||
|
c.SortOrder
|
||||||
|
FROM dbo.Chapters c
|
||||||
|
WHERE c.BookID = @BookID
|
||||||
|
AND c.IsArchived = 0
|
||||||
|
ORDER BY c.SortOrder, c.ChapterNumber, c.ChapterID;
|
||||||
|
END;
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE OR ALTER PROCEDURE dbo.WordCompanion_Chapter_Create
|
||||||
|
@BookID int,
|
||||||
|
@UserID int,
|
||||||
|
@Title nvarchar(200),
|
||||||
|
@SortOrder int = NULL
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
|
||||||
|
DECLARE @TrimmedTitle nvarchar(200) = LTRIM(RTRIM(COALESCE(@Title, N'')));
|
||||||
|
IF NULLIF(@TrimmedTitle, N'') IS NULL OR @SortOrder <= 0
|
||||||
|
RETURN;
|
||||||
|
|
||||||
|
IF NOT EXISTS
|
||||||
|
(
|
||||||
|
SELECT 1
|
||||||
|
FROM dbo.Books b
|
||||||
|
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 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);
|
||||||
|
IF @PlannedRevisionStatusID IS NULL
|
||||||
|
RETURN;
|
||||||
|
|
||||||
|
DECLARE @NextChapterNumber decimal(9,2) = ISNULL((SELECT MAX(ChapterNumber) FROM dbo.Chapters WHERE BookID = @BookID), 0) + 1;
|
||||||
|
DECLARE @NextChapterOrder int = ISNULL((SELECT MAX(SortOrder) FROM dbo.Chapters WHERE BookID = @BookID), 0) + 10;
|
||||||
|
DECLARE @ChapterID int;
|
||||||
|
|
||||||
|
INSERT dbo.Chapters (BookID, ChapterNumber, ChapterTitle, Summary, SortOrder, RevisionStatusID)
|
||||||
|
VALUES (@BookID, @NextChapterNumber, @TrimmedTitle, NULL, COALESCE(@SortOrder, @NextChapterOrder), @PlannedRevisionStatusID);
|
||||||
|
|
||||||
|
SET @ChapterID = CAST(SCOPE_IDENTITY() AS int);
|
||||||
|
|
||||||
|
SELECT @ChapterID AS ChapterId,
|
||||||
|
@TrimmedTitle AS Title,
|
||||||
|
COALESCE(@SortOrder, @NextChapterOrder) AS SortOrder,
|
||||||
|
N'Chapter created successfully.' AS Message;
|
||||||
|
END;
|
||||||
|
GO
|
||||||
@ -121,6 +121,23 @@
|
|||||||
<p class="word-companion-message" data-sync-note></p>
|
<p class="word-companion-message" data-sync-note></p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section class="word-companion-section word-companion-create-link" data-chapter-create-link hidden aria-label="Chapter Create Link">
|
||||||
|
<div class="word-companion-section-header">
|
||||||
|
<span>Chapter Create / Link</span>
|
||||||
|
</div>
|
||||||
|
<div class="word-companion-scene-card">
|
||||||
|
<div>
|
||||||
|
<span>Current Chapter</span>
|
||||||
|
<strong data-chapter-create-link-chapter>-</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="word-companion-actions">
|
||||||
|
<button type="button" data-create-plotdirector-chapter disabled>Create Chapter in PlotDirector</button>
|
||||||
|
<button type="button" data-link-existing-chapter disabled>Link to Existing Chapter</button>
|
||||||
|
</div>
|
||||||
|
<p class="word-companion-message" data-chapter-create-link-status></p>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section class="word-companion-section word-companion-create-link" data-scene-create-link hidden aria-label="Scene Create Link">
|
<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">
|
<div class="word-companion-section-header">
|
||||||
<span>Scene Create / Link</span>
|
<span>Scene Create / Link</span>
|
||||||
@ -300,6 +317,20 @@
|
|||||||
</form>
|
</form>
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|
||||||
|
<dialog class="word-companion-dialog" data-link-existing-chapter-dialog>
|
||||||
|
<form method="dialog">
|
||||||
|
<div class="word-companion-section-header">
|
||||||
|
<span>Link to Existing Chapter</span>
|
||||||
|
<button type="button" data-link-chapter-dialog-cancel>Cancel</button>
|
||||||
|
</div>
|
||||||
|
<label for="word-companion-chapter-search">Search chapters</label>
|
||||||
|
<input id="word-companion-chapter-search" type="search" data-link-chapter-search autocomplete="off" />
|
||||||
|
<div class="word-companion-scene-options" data-link-chapter-options></div>
|
||||||
|
<button type="button" data-link-selected-chapter disabled>Link Chapter</button>
|
||||||
|
<p class="word-companion-message" data-link-chapter-dialog-status></p>
|
||||||
|
</form>
|
||||||
|
</dialog>
|
||||||
|
|
||||||
<dialog class="word-companion-dialog" data-create-scene-dialog>
|
<dialog class="word-companion-dialog" data-create-scene-dialog>
|
||||||
<form method="dialog">
|
<form method="dialog">
|
||||||
<div class="word-companion-section-header">
|
<div class="word-companion-section-header">
|
||||||
@ -315,6 +346,22 @@
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|
||||||
|
<dialog class="word-companion-dialog" data-create-chapter-dialog>
|
||||||
|
<form method="dialog">
|
||||||
|
<div class="word-companion-section-header">
|
||||||
|
<span>Create PlotDirector Chapter</span>
|
||||||
|
</div>
|
||||||
|
<p class="word-companion-dialog-copy">Create PlotDirector chapter:</p>
|
||||||
|
<strong data-create-chapter-dialog-chapter>-</strong>
|
||||||
|
<p class="word-companion-dialog-copy">in book:</p>
|
||||||
|
<strong data-create-chapter-dialog-book>-</strong>
|
||||||
|
<div class="word-companion-dialog-actions">
|
||||||
|
<button type="button" data-create-chapter-dialog-confirm>Create</button>
|
||||||
|
<button type="button" data-create-chapter-dialog-cancel>Cancel</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</dialog>
|
||||||
</main>
|
</main>
|
||||||
<script src="~/js/word-companion-host.js" asp-append-version="true"></script>
|
<script src="~/js/word-companion-host.js" asp-append-version="true"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@ -51,6 +51,11 @@
|
|||||||
const primaryLocationSelect = document.querySelector("[data-primary-location-select]");
|
const primaryLocationSelect = document.querySelector("[data-primary-location-select]");
|
||||||
const saveSceneLinksButton = document.querySelector("[data-save-scene-links]");
|
const saveSceneLinksButton = document.querySelector("[data-save-scene-links]");
|
||||||
const sceneLinksStatus = document.querySelector("[data-scene-links-status]");
|
const sceneLinksStatus = document.querySelector("[data-scene-links-status]");
|
||||||
|
const chapterCreateLink = document.querySelector("[data-chapter-create-link]");
|
||||||
|
const chapterCreateLinkChapter = document.querySelector("[data-chapter-create-link-chapter]");
|
||||||
|
const createPlotDirectorChapterButton = document.querySelector("[data-create-plotdirector-chapter]");
|
||||||
|
const linkExistingChapterButton = document.querySelector("[data-link-existing-chapter]");
|
||||||
|
const chapterCreateLinkStatus = document.querySelector("[data-chapter-create-link-status]");
|
||||||
const sceneCreateLink = document.querySelector("[data-scene-create-link]");
|
const sceneCreateLink = document.querySelector("[data-scene-create-link]");
|
||||||
const createLinkChapter = document.querySelector("[data-create-link-chapter]");
|
const createLinkChapter = document.querySelector("[data-create-link-chapter]");
|
||||||
const createLinkScene = document.querySelector("[data-create-link-scene]");
|
const createLinkScene = document.querySelector("[data-create-link-scene]");
|
||||||
@ -68,6 +73,17 @@
|
|||||||
const createDialogChapter = document.querySelector("[data-create-dialog-chapter]");
|
const createDialogChapter = document.querySelector("[data-create-dialog-chapter]");
|
||||||
const createDialogConfirm = document.querySelector("[data-create-dialog-confirm]");
|
const createDialogConfirm = document.querySelector("[data-create-dialog-confirm]");
|
||||||
const createDialogCancel = document.querySelector("[data-create-dialog-cancel]");
|
const createDialogCancel = document.querySelector("[data-create-dialog-cancel]");
|
||||||
|
const linkExistingChapterDialog = document.querySelector("[data-link-existing-chapter-dialog]");
|
||||||
|
const linkChapterSearch = document.querySelector("[data-link-chapter-search]");
|
||||||
|
const linkChapterOptions = document.querySelector("[data-link-chapter-options]");
|
||||||
|
const linkSelectedChapterButton = document.querySelector("[data-link-selected-chapter]");
|
||||||
|
const linkChapterDialogCancel = document.querySelector("[data-link-chapter-dialog-cancel]");
|
||||||
|
const linkChapterDialogStatus = document.querySelector("[data-link-chapter-dialog-status]");
|
||||||
|
const createChapterDialog = document.querySelector("[data-create-chapter-dialog]");
|
||||||
|
const createChapterDialogChapter = document.querySelector("[data-create-chapter-dialog-chapter]");
|
||||||
|
const createChapterDialogBook = document.querySelector("[data-create-chapter-dialog-book]");
|
||||||
|
const createChapterDialogConfirm = document.querySelector("[data-create-chapter-dialog-confirm]");
|
||||||
|
const createChapterDialogCancel = document.querySelector("[data-create-chapter-dialog-cancel]");
|
||||||
const resolveProjectId = document.querySelector("[data-resolve-project-id]");
|
const resolveProjectId = document.querySelector("[data-resolve-project-id]");
|
||||||
const resolveProjectTitle = document.querySelector("[data-resolve-project-title]");
|
const resolveProjectTitle = document.querySelector("[data-resolve-project-title]");
|
||||||
const resolveBookId = document.querySelector("[data-resolve-book-id]");
|
const resolveBookId = document.querySelector("[data-resolve-book-id]");
|
||||||
@ -103,6 +119,7 @@
|
|||||||
let detectedChapterTitle = "";
|
let detectedChapterTitle = "";
|
||||||
let detectedSceneTitle = "";
|
let detectedSceneTitle = "";
|
||||||
let detectedSceneWordCount = null;
|
let detectedSceneWordCount = null;
|
||||||
|
let detectedChapterIndex = null;
|
||||||
let detectedChapterAnchorId = null;
|
let detectedChapterAnchorId = null;
|
||||||
let detectedSceneAnchorId = null;
|
let detectedSceneAnchorId = null;
|
||||||
let resolvedSceneId = null;
|
let resolvedSceneId = null;
|
||||||
@ -113,6 +130,9 @@
|
|||||||
let linkDialogScenes = [];
|
let linkDialogScenes = [];
|
||||||
let selectedLinkSceneId = null;
|
let selectedLinkSceneId = null;
|
||||||
let pendingCreateSceneConfirmation = null;
|
let pendingCreateSceneConfirmation = null;
|
||||||
|
let linkDialogChapters = [];
|
||||||
|
let selectedLinkChapterId = null;
|
||||||
|
let pendingCreateChapterConfirmation = null;
|
||||||
let currentSceneLinks = {
|
let currentSceneLinks = {
|
||||||
characters: [],
|
characters: [],
|
||||||
assets: [],
|
assets: [],
|
||||||
@ -198,10 +218,11 @@
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const setCurrentStructure = (chapterTitle, sceneTitle, wordCount, chapterAnchorId = null, sceneAnchorId = null) => {
|
const setCurrentStructure = (chapterTitle, sceneTitle, wordCount, chapterAnchorId = null, sceneAnchorId = null, chapterIndex = null) => {
|
||||||
detectedChapterTitle = chapterTitle || "";
|
detectedChapterTitle = chapterTitle || "";
|
||||||
detectedSceneTitle = sceneTitle || "";
|
detectedSceneTitle = sceneTitle || "";
|
||||||
detectedSceneWordCount = Number.isInteger(wordCount) ? wordCount : null;
|
detectedSceneWordCount = Number.isInteger(wordCount) ? wordCount : null;
|
||||||
|
detectedChapterIndex = Number.isInteger(chapterIndex) && chapterIndex > 0 ? chapterIndex : null;
|
||||||
detectedChapterAnchorId = Number.isInteger(chapterAnchorId) && chapterAnchorId > 0 ? chapterAnchorId : null;
|
detectedChapterAnchorId = Number.isInteger(chapterAnchorId) && chapterAnchorId > 0 ? chapterAnchorId : null;
|
||||||
detectedSceneAnchorId = Number.isInteger(sceneAnchorId) && sceneAnchorId > 0 ? sceneAnchorId : null;
|
detectedSceneAnchorId = Number.isInteger(sceneAnchorId) && sceneAnchorId > 0 ? sceneAnchorId : null;
|
||||||
|
|
||||||
@ -268,11 +289,48 @@
|
|||||||
setText(createLinkStatus, message);
|
setText(createLinkStatus, message);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const setChapterCreateLinkStatus = (message) => {
|
||||||
|
setText(chapterCreateLinkStatus, message);
|
||||||
|
};
|
||||||
|
|
||||||
|
const canUseChapterCreateLinkActions = () => !!selectedBook()
|
||||||
|
&& !!detectedChapterTitle
|
||||||
|
&& !resolvedChapterId;
|
||||||
|
|
||||||
const canUseCreateLinkActions = () => !!selectedBook()
|
const canUseCreateLinkActions = () => !!selectedBook()
|
||||||
&& !!detectedSceneTitle
|
&& !!detectedSceneTitle
|
||||||
&& Number.isInteger(resolvedChapterId)
|
&& Number.isInteger(resolvedChapterId)
|
||||||
&& resolvedChapterId > 0;
|
&& resolvedChapterId > 0;
|
||||||
|
|
||||||
|
const hideChapterCreateLink = () => {
|
||||||
|
setHidden(chapterCreateLink, true);
|
||||||
|
setChapterCreateLinkStatus("");
|
||||||
|
if (createPlotDirectorChapterButton) {
|
||||||
|
createPlotDirectorChapterButton.disabled = true;
|
||||||
|
createPlotDirectorChapterButton.textContent = "Create Chapter in PlotDirector";
|
||||||
|
}
|
||||||
|
if (linkExistingChapterButton) {
|
||||||
|
linkExistingChapterButton.disabled = true;
|
||||||
|
linkExistingChapterButton.textContent = "Link to Existing Chapter";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const showChapterCreateLink = (message = "") => {
|
||||||
|
setText(chapterCreateLinkChapter, displayValue(detectedChapterTitle));
|
||||||
|
setHidden(chapterCreateLink, false);
|
||||||
|
setChapterCreateLinkStatus(message);
|
||||||
|
|
||||||
|
const enabled = canUseChapterCreateLinkActions();
|
||||||
|
if (createPlotDirectorChapterButton) {
|
||||||
|
createPlotDirectorChapterButton.disabled = !enabled;
|
||||||
|
createPlotDirectorChapterButton.textContent = "Create Chapter in PlotDirector";
|
||||||
|
}
|
||||||
|
if (linkExistingChapterButton) {
|
||||||
|
linkExistingChapterButton.disabled = !enabled;
|
||||||
|
linkExistingChapterButton.textContent = "Link to Existing Chapter";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const hideSceneCreateLink = () => {
|
const hideSceneCreateLink = () => {
|
||||||
setHidden(sceneCreateLink, true);
|
setHidden(sceneCreateLink, true);
|
||||||
setCreateLinkStatus("");
|
setCreateLinkStatus("");
|
||||||
@ -308,6 +366,7 @@
|
|||||||
setHidden(plotDirectorSceneDetails, true);
|
setHidden(plotDirectorSceneDetails, true);
|
||||||
setHidden(writingBriefBlock, true);
|
setHidden(writingBriefBlock, true);
|
||||||
setHidden(plotDirectorLinkGroups, true);
|
setHidden(plotDirectorLinkGroups, true);
|
||||||
|
hideChapterCreateLink();
|
||||||
hideSceneCreateLink();
|
hideSceneCreateLink();
|
||||||
currentResolutionMethod = "";
|
currentResolutionMethod = "";
|
||||||
currentResolutionAnchorType = detectedSceneAnchorId ? "SceneID Anchor" : detectedChapterAnchorId ? "ChapterID Anchor" : "Title Match";
|
currentResolutionAnchorType = detectedSceneAnchorId ? "SceneID Anchor" : detectedChapterAnchorId ? "ChapterID Anchor" : "Title Match";
|
||||||
@ -506,6 +565,11 @@
|
|||||||
return Array.isArray(structure?.chapters) ? structure.chapters : [];
|
return Array.isArray(structure?.chapters) ? structure.chapters : [];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const fetchBookChapters = async (bookId) => {
|
||||||
|
const response = await fetchJson(`/api/word-companion/books/${bookId}/chapters`);
|
||||||
|
return Array.isArray(response?.chapters) ? response.chapters : [];
|
||||||
|
};
|
||||||
|
|
||||||
const findCurrentChapterInChapters = (chapters) => {
|
const findCurrentChapterInChapters = (chapters) => {
|
||||||
if (Number.isInteger(resolvedChapterId) && resolvedChapterId > 0) {
|
if (Number.isInteger(resolvedChapterId) && resolvedChapterId > 0) {
|
||||||
const anchoredChapter = chapters.find((chapter) => chapter.chapterId === resolvedChapterId);
|
const anchoredChapter = chapters.find((chapter) => chapter.chapterId === resolvedChapterId);
|
||||||
@ -543,6 +607,7 @@
|
|||||||
storedAnchorInvalid = false;
|
storedAnchorInvalid = false;
|
||||||
setPlotDirectorSceneStatus("Linked to PlotDirector");
|
setPlotDirectorSceneStatus("Linked to PlotDirector");
|
||||||
setDocumentMessage("");
|
setDocumentMessage("");
|
||||||
|
hideChapterCreateLink();
|
||||||
hideSceneCreateLink();
|
hideSceneCreateLink();
|
||||||
setResolvedScene(sceneId, chapterId, resolutionMethod);
|
setResolvedScene(sceneId, chapterId, resolutionMethod);
|
||||||
renderCompanionData(companionData);
|
renderCompanionData(companionData);
|
||||||
@ -918,7 +983,8 @@
|
|||||||
structure.currentScene?.title,
|
structure.currentScene?.title,
|
||||||
structure.currentScene?.wordCount,
|
structure.currentScene?.wordCount,
|
||||||
structure.currentChapter?.anchorId,
|
structure.currentChapter?.anchorId,
|
||||||
structure.currentScene?.anchorId
|
structure.currentScene?.anchorId,
|
||||||
|
structure.currentChapter?.index
|
||||||
);
|
);
|
||||||
resetPlotDirectorScene("Not detected");
|
resetPlotDirectorScene("Not detected");
|
||||||
renderOutline(structure);
|
renderOutline(structure);
|
||||||
@ -1163,10 +1229,269 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const attachResolvedChapterId = async (successMessage) => {
|
||||||
|
if (!resolvedChapterId) {
|
||||||
|
setChapterCreateLinkStatus("No resolved PlotDirector chapter.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!officeReady || !wordHostAvailable || !window.Word || typeof window.Word.run !== "function") {
|
||||||
|
setChapterCreateLinkStatus("Word document access is unavailable.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await window.Word.run(async (context) => {
|
||||||
|
const structure = await readDocumentStructure(context);
|
||||||
|
if (!structure.currentChapter) {
|
||||||
|
throw new Error("No chapter detected in Word. Use Heading 1 for chapters.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const chapterParagraph = structure.bodyParagraphs[structure.currentChapter.paragraphIndex];
|
||||||
|
if (!chapterParagraph) {
|
||||||
|
throw new Error("Unable to locate the current Word chapter heading.");
|
||||||
|
}
|
||||||
|
|
||||||
|
ensureAnchorControl(chapterParagraph, "PlotDirector Chapter", `PD-CHAPTER-${resolvedChapterId}`, "PD-CHAPTER");
|
||||||
|
await context.sync();
|
||||||
|
});
|
||||||
|
|
||||||
|
detectedChapterAnchorId = resolvedChapterId;
|
||||||
|
storedAnchorInvalid = false;
|
||||||
|
currentResolutionAnchorType = "ChapterID Anchor";
|
||||||
|
currentResolutionMethod = "Chapter anchor";
|
||||||
|
setChapterCreateLinkStatus(successMessage);
|
||||||
|
setDocumentMessage(successMessage);
|
||||||
|
setDiagnostics({ lastError: "-" });
|
||||||
|
updateAnchorDisplay();
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Unable to attach PlotDirector chapter ID.", error);
|
||||||
|
setChapterCreateLinkStatus("Unable to attach PlotDirector chapter ID.");
|
||||||
|
setDiagnostics({ lastError: errorText(error) });
|
||||||
|
updateAnchorDisplay();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const attachPlotDirectorIds = async () => {
|
const attachPlotDirectorIds = async () => {
|
||||||
await attachResolvedPlotDirectorIds();
|
await attachResolvedPlotDirectorIds();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const refreshAfterChapterLink = async (message) => {
|
||||||
|
hideChapterCreateLink();
|
||||||
|
await refreshPlotDirectorScene();
|
||||||
|
setDocumentMessage(message);
|
||||||
|
setChapterCreateLinkStatus(message);
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeCreateChapterDialog = (confirmed) => {
|
||||||
|
if (pendingCreateChapterConfirmation) {
|
||||||
|
pendingCreateChapterConfirmation(confirmed);
|
||||||
|
pendingCreateChapterConfirmation = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (createChapterDialog?.close) {
|
||||||
|
createChapterDialog.close();
|
||||||
|
} else if (createChapterDialog) {
|
||||||
|
createChapterDialog.hidden = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const confirmCreateChapter = (chapterTitle, bookTitle) => {
|
||||||
|
setText(createChapterDialogChapter, `"${chapterTitle}"`);
|
||||||
|
setText(createChapterDialogBook, `"${bookTitle}"`);
|
||||||
|
|
||||||
|
if (!createChapterDialog) {
|
||||||
|
return Promise.resolve(window.confirm(`Create PlotDirector chapter:\n\n"${chapterTitle}"\n\nin book:\n\n"${bookTitle}"\n\n?`));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
pendingCreateChapterConfirmation = resolve;
|
||||||
|
if (createChapterDialog.showModal) {
|
||||||
|
createChapterDialog.showModal();
|
||||||
|
} else {
|
||||||
|
createChapterDialog.hidden = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const createPlotDirectorChapter = async () => {
|
||||||
|
const book = selectedBook();
|
||||||
|
if (!book || !detectedChapterTitle || resolvedChapterId) {
|
||||||
|
showChapterCreateLink("Chapter not found in PlotDirector.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const chapterTitle = normalizeWhitespace(detectedChapterTitle);
|
||||||
|
const confirmed = await confirmCreateChapter(chapterTitle, book.title || "selected book");
|
||||||
|
if (!confirmed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (createPlotDirectorChapterButton) {
|
||||||
|
createPlotDirectorChapterButton.disabled = true;
|
||||||
|
createPlotDirectorChapterButton.textContent = "Creating...";
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const sortOrder = Number.isInteger(detectedChapterIndex) && detectedChapterIndex > 0
|
||||||
|
? detectedChapterIndex * 10
|
||||||
|
: null;
|
||||||
|
const response = await postJson(`/api/word-companion/books/${book.bookId}/chapters`, {
|
||||||
|
title: chapterTitle,
|
||||||
|
sortOrder
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response?.chapterId) {
|
||||||
|
throw new Error("Chapter creation did not return a chapter ID.");
|
||||||
|
}
|
||||||
|
|
||||||
|
setResolvedScene(null, response.chapterId, "Manual chapter link");
|
||||||
|
const anchored = await attachResolvedChapterId("Chapter created and linked successfully.");
|
||||||
|
if (!anchored) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await refreshAfterChapterLink("Chapter created and linked successfully.");
|
||||||
|
} catch (error) {
|
||||||
|
setChapterCreateLinkStatus("Unable to create chapter.");
|
||||||
|
setDiagnostics({ lastError: errorText(error) });
|
||||||
|
} finally {
|
||||||
|
if (createPlotDirectorChapterButton) {
|
||||||
|
createPlotDirectorChapterButton.disabled = !canUseChapterCreateLinkActions();
|
||||||
|
createPlotDirectorChapterButton.textContent = "Create Chapter in PlotDirector";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderLinkChapterOptions = () => {
|
||||||
|
if (!linkChapterOptions) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = normalizeWhitespace(linkChapterSearch?.value || "").toLocaleLowerCase();
|
||||||
|
const rows = linkDialogChapters.filter((chapter) => !query || String(chapter.title || "").toLocaleLowerCase().includes(query));
|
||||||
|
linkChapterOptions.innerHTML = "";
|
||||||
|
selectedLinkChapterId = null;
|
||||||
|
if (linkSelectedChapterButton) {
|
||||||
|
linkSelectedChapterButton.disabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rows.length === 0) {
|
||||||
|
const empty = document.createElement("p");
|
||||||
|
empty.className = "word-companion-empty-list";
|
||||||
|
empty.textContent = "No chapters found.";
|
||||||
|
linkChapterOptions.append(empty);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const chapter of rows) {
|
||||||
|
const chapterId = Number.parseInt(chapter.chapterId || "", 10);
|
||||||
|
if (!Number.isInteger(chapterId) || chapterId <= 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-chapter";
|
||||||
|
radio.value = String(chapterId);
|
||||||
|
radio.addEventListener("change", () => {
|
||||||
|
selectedLinkChapterId = chapterId;
|
||||||
|
if (linkSelectedChapterButton) {
|
||||||
|
linkSelectedChapterButton.disabled = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const text = document.createElement("span");
|
||||||
|
text.textContent = chapter.title || "Untitled chapter";
|
||||||
|
|
||||||
|
label.append(radio, text);
|
||||||
|
linkChapterOptions.append(label);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const openLinkExistingChapterDialog = async () => {
|
||||||
|
const book = selectedBook();
|
||||||
|
if (!book || !detectedChapterTitle || resolvedChapterId) {
|
||||||
|
showChapterCreateLink("Chapter not found in PlotDirector.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (linkExistingChapterButton) {
|
||||||
|
linkExistingChapterButton.disabled = true;
|
||||||
|
linkExistingChapterButton.textContent = "Loading...";
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
linkDialogChapters = await fetchBookChapters(book.bookId);
|
||||||
|
selectedLinkChapterId = null;
|
||||||
|
if (linkChapterSearch) {
|
||||||
|
linkChapterSearch.value = "";
|
||||||
|
}
|
||||||
|
setText(linkChapterDialogStatus, "");
|
||||||
|
renderLinkChapterOptions();
|
||||||
|
|
||||||
|
if (linkExistingChapterDialog?.showModal) {
|
||||||
|
linkExistingChapterDialog.showModal();
|
||||||
|
} else if (linkExistingChapterDialog) {
|
||||||
|
linkExistingChapterDialog.hidden = false;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
setChapterCreateLinkStatus("Unable to load chapters.");
|
||||||
|
setDiagnostics({ lastError: errorText(error) });
|
||||||
|
} finally {
|
||||||
|
if (linkExistingChapterButton) {
|
||||||
|
linkExistingChapterButton.disabled = !canUseChapterCreateLinkActions();
|
||||||
|
linkExistingChapterButton.textContent = "Link to Existing Chapter";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeLinkExistingChapterDialog = () => {
|
||||||
|
if (linkExistingChapterDialog?.close) {
|
||||||
|
linkExistingChapterDialog.close();
|
||||||
|
} else if (linkExistingChapterDialog) {
|
||||||
|
linkExistingChapterDialog.hidden = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const linkSelectedExistingChapter = async () => {
|
||||||
|
const chapter = linkDialogChapters.find((item) => item.chapterId === selectedLinkChapterId);
|
||||||
|
if (!chapter) {
|
||||||
|
setText(linkChapterDialogStatus, "Select a chapter.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (linkSelectedChapterButton) {
|
||||||
|
linkSelectedChapterButton.disabled = true;
|
||||||
|
linkSelectedChapterButton.textContent = "Linking...";
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setResolvedScene(null, chapter.chapterId, "Manual chapter link");
|
||||||
|
const anchored = await attachResolvedChapterId("Chapter linked successfully.");
|
||||||
|
if (!anchored) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
closeLinkExistingChapterDialog();
|
||||||
|
await refreshAfterChapterLink("Chapter linked successfully.");
|
||||||
|
} catch (error) {
|
||||||
|
setText(linkChapterDialogStatus, "Unable to link chapter.");
|
||||||
|
setChapterCreateLinkStatus("Unable to link chapter.");
|
||||||
|
setDiagnostics({ lastError: errorText(error) });
|
||||||
|
} finally {
|
||||||
|
if (linkSelectedChapterButton) {
|
||||||
|
linkSelectedChapterButton.disabled = !selectedLinkChapterId;
|
||||||
|
linkSelectedChapterButton.textContent = "Link Chapter";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const resolveCreatedOrLinkedScene = async (sceneId, chapterId, successMessage) => {
|
const resolveCreatedOrLinkedScene = async (sceneId, chapterId, successMessage) => {
|
||||||
const book = selectedBook();
|
const book = selectedBook();
|
||||||
if (!book) {
|
if (!book) {
|
||||||
@ -1432,6 +1757,80 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const refreshPlotDirectorChapter = async () => {
|
||||||
|
const project = selectedProject();
|
||||||
|
const book = selectedBook();
|
||||||
|
const requestChapterTitle = normalizeTitleForResolve(detectedChapterTitle, "chapter");
|
||||||
|
setResolveDiagnostics({
|
||||||
|
projectId: project?.projectId,
|
||||||
|
projectTitle: project?.title,
|
||||||
|
bookId: book?.bookId,
|
||||||
|
bookTitle: book?.title,
|
||||||
|
detectedChapter: detectedChapterTitle,
|
||||||
|
detectedScene: detectedSceneTitle,
|
||||||
|
requestChapter: requestChapterTitle,
|
||||||
|
requestScene: "",
|
||||||
|
result: "Not run",
|
||||||
|
chapterId: null,
|
||||||
|
sceneId: null
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!book) {
|
||||||
|
resetPlotDirectorScene("Not detected");
|
||||||
|
setDocumentMessage("Select a PlotDirector book first.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!detectedChapterTitle) {
|
||||||
|
resetPlotDirectorScene("Not detected");
|
||||||
|
setDocumentMessage("No chapter detected in Word. Use Heading 1 for chapters.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
resetPlotDirectorScene("Not detected");
|
||||||
|
try {
|
||||||
|
const chapters = await fetchBookChapters(book.bookId);
|
||||||
|
const anchoredChapter = detectedChapterAnchorId
|
||||||
|
? chapters.find((chapter) => chapter.chapterId === detectedChapterAnchorId)
|
||||||
|
: null;
|
||||||
|
const requestChapterKey = requestChapterTitle.toLocaleLowerCase();
|
||||||
|
const titleChapter = requestChapterKey
|
||||||
|
? chapters.find((chapter) => normalizeTitleForResolve(chapter.title, "chapter").toLocaleLowerCase() === requestChapterKey)
|
||||||
|
: null;
|
||||||
|
const chapter = anchoredChapter || titleChapter;
|
||||||
|
|
||||||
|
if (chapter) {
|
||||||
|
hideChapterCreateLink();
|
||||||
|
setResolvedScene(null, chapter.chapterId, anchoredChapter ? "Chapter anchor" : "Chapter title");
|
||||||
|
currentResolutionAnchorType = anchoredChapter ? "ChapterID Anchor" : "Title Match";
|
||||||
|
setPlotDirectorSceneStatus("Chapter linked to PlotDirector");
|
||||||
|
setDocumentMessage("No scene detected in Word. Use Heading 2 for scenes.");
|
||||||
|
setResolveDiagnostics({
|
||||||
|
result: "Chapter matched",
|
||||||
|
chapterId: chapter.chapterId,
|
||||||
|
sceneId: null
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
setResolvedScene(null);
|
||||||
|
setPlotDirectorSceneStatus("Chapter not found in PlotDirector.");
|
||||||
|
setDocumentMessage("Chapter not found in PlotDirector.");
|
||||||
|
setResolveDiagnostics({
|
||||||
|
result: "Chapter not matched",
|
||||||
|
chapterId: null,
|
||||||
|
sceneId: null
|
||||||
|
});
|
||||||
|
showChapterCreateLink("Chapter not found in PlotDirector.");
|
||||||
|
return false;
|
||||||
|
} catch (error) {
|
||||||
|
resetPlotDirectorScene("Not found in PlotDirector");
|
||||||
|
setDiagnostics({ lastError: errorText(error) });
|
||||||
|
setDocumentMessage("Unable to load PlotDirector chapter data.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const refreshPlotDirectorScene = async () => {
|
const refreshPlotDirectorScene = async () => {
|
||||||
const project = selectedProject();
|
const project = selectedProject();
|
||||||
const book = selectedBook();
|
const book = selectedBook();
|
||||||
@ -1546,12 +1945,9 @@
|
|||||||
sceneId: null
|
sceneId: null
|
||||||
});
|
});
|
||||||
} else if (!detectedSceneAnchorId) {
|
} else if (!detectedSceneAnchorId) {
|
||||||
storedAnchorInvalid = true;
|
|
||||||
currentResolutionAnchorType = "ChapterID Anchor";
|
currentResolutionAnchorType = "ChapterID Anchor";
|
||||||
setPlotDirectorSceneStatus("Not found in PlotDirector");
|
|
||||||
setDocumentMessage("Stored PlotDirector ID is invalid.");
|
|
||||||
setResolveDiagnostics({
|
setResolveDiagnostics({
|
||||||
result: "Error",
|
result: "Chapter anchor not found",
|
||||||
chapterId: detectedChapterAnchorId,
|
chapterId: detectedChapterAnchorId,
|
||||||
sceneId: null
|
sceneId: null
|
||||||
});
|
});
|
||||||
@ -1586,9 +1982,13 @@
|
|||||||
sceneId: resolveResponse?.sceneId
|
sceneId: resolveResponse?.sceneId
|
||||||
});
|
});
|
||||||
if (!hadInvalidAnchor) {
|
if (!hadInvalidAnchor) {
|
||||||
showSceneCreateLink(chapterMatched
|
if (chapterMatched) {
|
||||||
? "Scene not found in PlotDirector."
|
hideChapterCreateLink();
|
||||||
: "This chapter does not exist in PlotDirector. Create or link the chapter first.");
|
showSceneCreateLink("Scene not found in PlotDirector.");
|
||||||
|
} else {
|
||||||
|
hideSceneCreateLink();
|
||||||
|
showChapterCreateLink("Chapter not found in PlotDirector.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
restorePlotDirectorSceneButton();
|
restorePlotDirectorSceneButton();
|
||||||
return false;
|
return false;
|
||||||
@ -1636,8 +2036,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!detectedSceneTitle) {
|
if (!detectedSceneTitle) {
|
||||||
resetPlotDirectorScene("Not detected");
|
await refreshPlotDirectorChapter();
|
||||||
setDocumentMessage("No scene detected in Word. Use Heading 2 for scenes.");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1759,6 +2158,17 @@
|
|||||||
syncSceneProgressButton?.addEventListener("click", syncSceneProgress);
|
syncSceneProgressButton?.addEventListener("click", syncSceneProgress);
|
||||||
saveSceneLinksButton?.addEventListener("click", saveSceneLinks);
|
saveSceneLinksButton?.addEventListener("click", saveSceneLinks);
|
||||||
attachPlotDirectorIdsButton?.addEventListener("click", attachPlotDirectorIds);
|
attachPlotDirectorIdsButton?.addEventListener("click", attachPlotDirectorIds);
|
||||||
|
createPlotDirectorChapterButton?.addEventListener("click", createPlotDirectorChapter);
|
||||||
|
linkExistingChapterButton?.addEventListener("click", openLinkExistingChapterDialog);
|
||||||
|
linkChapterSearch?.addEventListener("input", renderLinkChapterOptions);
|
||||||
|
linkSelectedChapterButton?.addEventListener("click", linkSelectedExistingChapter);
|
||||||
|
linkChapterDialogCancel?.addEventListener("click", closeLinkExistingChapterDialog);
|
||||||
|
createChapterDialogConfirm?.addEventListener("click", () => closeCreateChapterDialog(true));
|
||||||
|
createChapterDialogCancel?.addEventListener("click", () => closeCreateChapterDialog(false));
|
||||||
|
createChapterDialog?.addEventListener("cancel", (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
closeCreateChapterDialog(false);
|
||||||
|
});
|
||||||
createPlotDirectorSceneButton?.addEventListener("click", createPlotDirectorScene);
|
createPlotDirectorSceneButton?.addEventListener("click", createPlotDirectorScene);
|
||||||
linkExistingSceneButton?.addEventListener("click", openLinkExistingSceneDialog);
|
linkExistingSceneButton?.addEventListener("click", openLinkExistingSceneDialog);
|
||||||
linkSceneSearch?.addEventListener("input", renderLinkSceneOptions);
|
linkSceneSearch?.addEventListener("input", renderLinkSceneOptions);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user