Phase 16C - Word Companion First-Run Binding and Manuscript Import
This commit is contained in:
parent
ced6f4cd0e
commit
33e8f0857c
@ -20,6 +20,13 @@ public sealed class WordCompanionController(IWordCompanionService wordCompanion)
|
||||
return response is null ? BadRequest() : Ok(response);
|
||||
}
|
||||
|
||||
[HttpPost("projects/{projectId:int}/books")]
|
||||
public async Task<IActionResult> CreateBook(int projectId, WordCompanionCreateBookRequest request)
|
||||
{
|
||||
var response = await wordCompanion.CreateBookAsync(projectId, request);
|
||||
return response is null ? BadRequest() : Ok(response);
|
||||
}
|
||||
|
||||
[HttpGet("books/{bookId:int}/structure")]
|
||||
public async Task<IActionResult> GetBookStructure(int bookId)
|
||||
{
|
||||
@ -108,4 +115,18 @@ public sealed class WordCompanionController(IWordCompanionService wordCompanion)
|
||||
var response = await wordCompanion.LinkManuscriptAsync(request);
|
||||
return response is null ? BadRequest() : Ok(response);
|
||||
}
|
||||
|
||||
[HttpPost("manuscript/analyse")]
|
||||
public async Task<IActionResult> AnalyseManuscript(WordCompanionManuscriptAnalyseRequest request)
|
||||
{
|
||||
var response = await wordCompanion.AnalyseManuscriptAsync(request);
|
||||
return response is null ? BadRequest() : Ok(response);
|
||||
}
|
||||
|
||||
[HttpPost("manuscript/import")]
|
||||
public async Task<IActionResult> ImportManuscript(WordCompanionManuscriptImportRequest request)
|
||||
{
|
||||
var response = await wordCompanion.ImportManuscriptAsync(request);
|
||||
return response is null ? BadRequest() : Ok(response);
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,6 +8,7 @@ public interface IWordCompanionRepository
|
||||
{
|
||||
Task<IReadOnlyList<WordCompanionProjectDto>> ListProjectsAsync(int userId);
|
||||
Task<IReadOnlyList<WordCompanionBookDto>> ListBooksAsync(int projectId, int userId);
|
||||
Task<WordCompanionCreateBookResponse?> CreateBookAsync(int projectId, int userId, WordCompanionCreateBookRequest request);
|
||||
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);
|
||||
@ -18,6 +19,8 @@ public interface IWordCompanionRepository
|
||||
Task<WordCompanionSyncManuscriptResponse?> SyncManuscriptAsync(int bookId, int userId, WordCompanionSyncManuscriptRequest request);
|
||||
Task<WordCompanionUpdateSceneLinksResponse?> UpdateSceneLinksAsync(int sceneId, int userId, WordCompanionUpdateSceneLinksRequest request);
|
||||
Task<WordCompanionUpdateWordCountResponse?> UpdateWordCountAsync(int sceneId, int userId, int actualWords, DateTime? lastWorkedOn);
|
||||
Task<WordCompanionManuscriptAnalyseResponse?> AnalyseManuscriptAsync(int userId, WordCompanionManuscriptAnalyseRequest request);
|
||||
Task<WordCompanionManuscriptImportResponse?> ImportManuscriptAsync(int userId, WordCompanionManuscriptImportRequest request);
|
||||
}
|
||||
|
||||
public sealed class WordCompanionRepository(ISqlConnectionFactory connectionFactory) : IWordCompanionRepository
|
||||
@ -42,6 +45,15 @@ public sealed class WordCompanionRepository(ISqlConnectionFactory connectionFact
|
||||
return rows.ToList();
|
||||
}
|
||||
|
||||
public async Task<WordCompanionCreateBookResponse?> CreateBookAsync(int projectId, int userId, WordCompanionCreateBookRequest request)
|
||||
{
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
return await connection.QuerySingleOrDefaultAsync<WordCompanionCreateBookResponse>(
|
||||
"dbo.WordCompanion_Book_Create",
|
||||
new { ProjectID = projectId, UserID = userId, request.Title },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
}
|
||||
|
||||
public async Task<WordCompanionBookStructureResponse?> GetBookStructureAsync(int bookId, int userId)
|
||||
{
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
@ -221,6 +233,76 @@ public sealed class WordCompanionRepository(ISqlConnectionFactory connectionFact
|
||||
commandType: CommandType.StoredProcedure);
|
||||
}
|
||||
|
||||
public async Task<WordCompanionManuscriptAnalyseResponse?> AnalyseManuscriptAsync(int userId, WordCompanionManuscriptAnalyseRequest request)
|
||||
{
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
return await connection.QuerySingleOrDefaultAsync<WordCompanionManuscriptAnalyseResponse>(
|
||||
"dbo.WordCompanion_Manuscript_Analyse",
|
||||
new
|
||||
{
|
||||
UserID = userId,
|
||||
request.ProjectId,
|
||||
request.BookId,
|
||||
DocumentGuid = request.DocumentGuid,
|
||||
request.ChapterCount,
|
||||
request.SceneCount,
|
||||
request.WordCount
|
||||
},
|
||||
commandType: CommandType.StoredProcedure);
|
||||
}
|
||||
|
||||
public async Task<WordCompanionManuscriptImportResponse?> ImportManuscriptAsync(int userId, WordCompanionManuscriptImportRequest request)
|
||||
{
|
||||
var chapters = request.Chapters.Select(chapter => new
|
||||
{
|
||||
chapter.Title,
|
||||
chapter.SortOrder,
|
||||
chapter.WordCount,
|
||||
Scenes = chapter.Scenes.Select(scene => new
|
||||
{
|
||||
scene.Title,
|
||||
scene.SortOrder,
|
||||
scene.WordCount
|
||||
})
|
||||
});
|
||||
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
using var result = await connection.QueryMultipleAsync(
|
||||
"dbo.WordCompanion_Manuscript_Import",
|
||||
new
|
||||
{
|
||||
UserID = userId,
|
||||
request.ProjectId,
|
||||
request.BookId,
|
||||
DocumentGuid = request.DocumentGuid.GetValueOrDefault(),
|
||||
request.BindingVersion,
|
||||
request.ReplacePlanned,
|
||||
StructureJson = System.Text.Json.JsonSerializer.Serialize(chapters)
|
||||
},
|
||||
commandType: CommandType.StoredProcedure);
|
||||
|
||||
var response = await result.ReadSingleOrDefaultAsync<WordCompanionManuscriptImportResponse>();
|
||||
if (response is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var document = await result.ReadSingleOrDefaultAsync<WordCompanionManuscriptDocumentDto>();
|
||||
return new WordCompanionManuscriptImportResponse
|
||||
{
|
||||
Imported = response.Imported,
|
||||
RequiresReplaceConfirmation = response.RequiresReplaceConfirmation,
|
||||
Message = response.Message,
|
||||
ProjectId = response.ProjectId,
|
||||
BookId = response.BookId,
|
||||
ChaptersCreated = response.ChaptersCreated,
|
||||
ScenesCreated = response.ScenesCreated,
|
||||
ArchivedChapters = response.ArchivedChapters,
|
||||
ArchivedScenes = response.ArchivedScenes,
|
||||
ManuscriptDocument = document
|
||||
};
|
||||
}
|
||||
|
||||
private static string ToCsv(IEnumerable<int> ids) => string.Join(',', ids.Where(id => id > 0).Distinct());
|
||||
|
||||
private sealed class WordCompanionSceneStructureRow
|
||||
|
||||
@ -17,6 +17,7 @@ public sealed class WordCompanionBookDto
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public string? Subtitle { get; set; }
|
||||
public int SortOrder { get; set; }
|
||||
public bool UsesExplicitScenes { get; set; }
|
||||
}
|
||||
|
||||
public sealed class WordCompanionBooksResponse
|
||||
@ -249,3 +250,86 @@ public sealed class WordCompanionManuscriptLinkResponse
|
||||
public WordCompanionManuscriptDocumentDto ManuscriptDocument { get; init; } = new();
|
||||
public string Message { get; init; } = "Manuscript linked.";
|
||||
}
|
||||
|
||||
public sealed class WordCompanionCreateBookRequest
|
||||
{
|
||||
public string? Title { get; set; }
|
||||
}
|
||||
|
||||
public sealed class WordCompanionCreateBookResponse
|
||||
{
|
||||
public int BookId { get; init; }
|
||||
public string Title { get; init; } = string.Empty;
|
||||
public string? Subtitle { get; init; }
|
||||
public int SortOrder { get; init; }
|
||||
public bool UsesExplicitScenes { get; init; }
|
||||
public string Message { get; init; } = "Book created.";
|
||||
}
|
||||
|
||||
public sealed class WordCompanionManuscriptImportSceneRequest
|
||||
{
|
||||
public string? Title { get; set; }
|
||||
public int? SortOrder { get; set; }
|
||||
public int WordCount { get; set; }
|
||||
}
|
||||
|
||||
public sealed class WordCompanionManuscriptImportChapterRequest
|
||||
{
|
||||
public string? Title { get; set; }
|
||||
public int? SortOrder { get; set; }
|
||||
public int WordCount { get; set; }
|
||||
public IReadOnlyList<WordCompanionManuscriptImportSceneRequest> Scenes { get; set; } = [];
|
||||
}
|
||||
|
||||
public sealed class WordCompanionManuscriptAnalyseRequest
|
||||
{
|
||||
public int ProjectId { get; set; }
|
||||
public int BookId { get; set; }
|
||||
public Guid? DocumentGuid { get; set; }
|
||||
public int ChapterCount { get; set; }
|
||||
public int SceneCount { get; set; }
|
||||
public int WordCount { get; set; }
|
||||
}
|
||||
|
||||
public sealed class WordCompanionManuscriptAnalyseResponse
|
||||
{
|
||||
public int ProjectId { get; init; }
|
||||
public int BookId { get; init; }
|
||||
public string ProjectTitle { get; init; } = string.Empty;
|
||||
public string BookTitle { get; init; } = string.Empty;
|
||||
public string? BookSubtitle { get; init; }
|
||||
public bool UsesExplicitScenes { get; init; }
|
||||
public int ChapterCount { get; init; }
|
||||
public int SceneCount { get; init; }
|
||||
public int WordCount { get; init; }
|
||||
public int ExistingChapterCount { get; init; }
|
||||
public int ExistingSceneCount { get; init; }
|
||||
public int LinkedChapterCount { get; init; }
|
||||
public bool CanImport { get; init; }
|
||||
public bool RequiresReplaceConfirmation { get; init; }
|
||||
public string Message { get; init; } = string.Empty;
|
||||
}
|
||||
|
||||
public sealed class WordCompanionManuscriptImportRequest
|
||||
{
|
||||
public int ProjectId { get; set; }
|
||||
public int BookId { get; set; }
|
||||
public Guid? DocumentGuid { get; set; }
|
||||
public int BindingVersion { get; set; } = 1;
|
||||
public bool ReplacePlanned { get; set; }
|
||||
public IReadOnlyList<WordCompanionManuscriptImportChapterRequest> Chapters { get; set; } = [];
|
||||
}
|
||||
|
||||
public sealed class WordCompanionManuscriptImportResponse
|
||||
{
|
||||
public bool Imported { get; init; }
|
||||
public bool RequiresReplaceConfirmation { get; init; }
|
||||
public string Message { get; init; } = string.Empty;
|
||||
public int ProjectId { get; init; }
|
||||
public int BookId { get; init; }
|
||||
public int ChaptersCreated { get; init; }
|
||||
public int ScenesCreated { get; init; }
|
||||
public int ArchivedChapters { get; init; }
|
||||
public int ArchivedScenes { get; init; }
|
||||
public WordCompanionManuscriptDocumentDto? ManuscriptDocument { get; init; }
|
||||
}
|
||||
|
||||
@ -7,6 +7,7 @@ public interface IWordCompanionService
|
||||
{
|
||||
Task<WordCompanionProjectsResponse> ListProjectsAsync();
|
||||
Task<WordCompanionBooksResponse?> ListBooksAsync(int projectId);
|
||||
Task<WordCompanionCreateBookResponse?> CreateBookAsync(int projectId, WordCompanionCreateBookRequest request);
|
||||
Task<WordCompanionBookStructureResponse?> GetBookStructureAsync(int bookId);
|
||||
Task<WordCompanionChaptersResponse?> ListChaptersAsync(int bookId);
|
||||
Task<WordCompanionResolveSceneResponse?> ResolveSceneAsync(int bookId, WordCompanionResolveSceneRequest request);
|
||||
@ -19,6 +20,8 @@ public interface IWordCompanionService
|
||||
Task<WordCompanionUpdateWordCountResponse?> UpdateWordCountAsync(int sceneId, WordCompanionUpdateWordCountRequest request);
|
||||
Task<WordCompanionManuscriptBookResponse?> GetManuscriptForBookAsync(int bookId);
|
||||
Task<WordCompanionManuscriptLinkResponse?> LinkManuscriptAsync(WordCompanionManuscriptLinkRequest request);
|
||||
Task<WordCompanionManuscriptAnalyseResponse?> AnalyseManuscriptAsync(WordCompanionManuscriptAnalyseRequest request);
|
||||
Task<WordCompanionManuscriptImportResponse?> ImportManuscriptAsync(WordCompanionManuscriptImportRequest request);
|
||||
}
|
||||
|
||||
public sealed class WordCompanionService(
|
||||
@ -43,6 +46,17 @@ public sealed class WordCompanionService(
|
||||
return new WordCompanionBooksResponse { Books = books };
|
||||
}
|
||||
|
||||
public Task<WordCompanionCreateBookResponse?> CreateBookAsync(int projectId, WordCompanionCreateBookRequest request)
|
||||
{
|
||||
if (projectId <= 0 || string.IsNullOrWhiteSpace(request.Title))
|
||||
{
|
||||
return Task.FromResult<WordCompanionCreateBookResponse?>(null);
|
||||
}
|
||||
|
||||
request.Title = request.Title.Trim();
|
||||
return repository.CreateBookAsync(projectId, RequireUserId(), request);
|
||||
}
|
||||
|
||||
public Task<WordCompanionBookStructureResponse?> GetBookStructureAsync(int bookId)
|
||||
{
|
||||
return bookId <= 0 ? Task.FromResult<WordCompanionBookStructureResponse?>(null) : repository.GetBookStructureAsync(bookId, RequireUserId());
|
||||
@ -206,6 +220,50 @@ public sealed class WordCompanionService(
|
||||
};
|
||||
}
|
||||
|
||||
public Task<WordCompanionManuscriptAnalyseResponse?> AnalyseManuscriptAsync(WordCompanionManuscriptAnalyseRequest request)
|
||||
{
|
||||
if (request.ProjectId <= 0
|
||||
|| request.BookId <= 0
|
||||
|| request.ChapterCount < 0
|
||||
|| request.SceneCount < 0
|
||||
|| request.WordCount < 0)
|
||||
{
|
||||
return Task.FromResult<WordCompanionManuscriptAnalyseResponse?>(null);
|
||||
}
|
||||
|
||||
return repository.AnalyseManuscriptAsync(RequireUserId(), request);
|
||||
}
|
||||
|
||||
public Task<WordCompanionManuscriptImportResponse?> ImportManuscriptAsync(WordCompanionManuscriptImportRequest request)
|
||||
{
|
||||
if (request.ProjectId <= 0
|
||||
|| request.BookId <= 0
|
||||
|| request.BindingVersion <= 0
|
||||
|| request.Chapters.Count == 0
|
||||
|| request.Chapters.Any(chapter =>
|
||||
string.IsNullOrWhiteSpace(chapter.Title)
|
||||
|| chapter.SortOrder is <= 0
|
||||
|| chapter.WordCount < 0
|
||||
|| chapter.Scenes.Count == 0
|
||||
|| chapter.Scenes.Any(scene => string.IsNullOrWhiteSpace(scene.Title) || scene.SortOrder is <= 0 || scene.WordCount < 0)))
|
||||
{
|
||||
return Task.FromResult<WordCompanionManuscriptImportResponse?>(null);
|
||||
}
|
||||
|
||||
request.DocumentGuid = request.DocumentGuid.GetValueOrDefault() == Guid.Empty ? Guid.NewGuid() : request.DocumentGuid;
|
||||
|
||||
foreach (var chapter in request.Chapters)
|
||||
{
|
||||
chapter.Title = chapter.Title?.Trim();
|
||||
foreach (var scene in chapter.Scenes)
|
||||
{
|
||||
scene.Title = scene.Title?.Trim();
|
||||
}
|
||||
}
|
||||
|
||||
return repository.ImportManuscriptAsync(RequireUserId(), request);
|
||||
}
|
||||
|
||||
private async Task<WordCompanionResolveSceneResponse?> ResolveSceneCoreAsync(int bookId, WordCompanionResolveSceneRequest request)
|
||||
{
|
||||
return await repository.ResolveSceneAsync(bookId, RequireUserId(), request.ChapterTitle!.Trim(), request.SceneTitle!.Trim());
|
||||
|
||||
495
PlotLine/Sql/095_Phase16C_WordCompanionFirstRunImport.sql
Normal file
495
PlotLine/Sql/095_Phase16C_WordCompanionFirstRunImport.sql
Normal file
@ -0,0 +1,495 @@
|
||||
SET ANSI_NULLS ON;
|
||||
GO
|
||||
SET QUOTED_IDENTIFIER ON;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.WordCompanion_Book_ListByProject
|
||||
@ProjectID int,
|
||||
@UserID int
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
SELECT b.BookID AS BookId,
|
||||
b.BookTitle AS Title,
|
||||
b.Subtitle,
|
||||
b.SortOrder,
|
||||
b.UsesExplicitScenes
|
||||
FROM dbo.Books b
|
||||
INNER JOIN dbo.ProjectUserAccess pua ON pua.ProjectID = b.ProjectID
|
||||
INNER JOIN dbo.Projects p ON p.ProjectID = b.ProjectID
|
||||
WHERE b.ProjectID = @ProjectID
|
||||
AND pua.UserID = @UserID
|
||||
AND pua.IsActive = 1
|
||||
AND p.IsArchived = 0
|
||||
AND b.IsArchived = 0
|
||||
ORDER BY b.SortOrder, b.BookNumber, b.BookTitle, b.BookID;
|
||||
END;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.WordCompanion_Book_Create
|
||||
@ProjectID int,
|
||||
@UserID int,
|
||||
@Title nvarchar(200)
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
DECLARE @TrimmedTitle nvarchar(200) = LTRIM(RTRIM(COALESCE(@Title, N'')));
|
||||
IF NULLIF(@TrimmedTitle, N'') IS NULL
|
||||
RETURN;
|
||||
|
||||
IF NOT EXISTS
|
||||
(
|
||||
SELECT 1
|
||||
FROM dbo.Projects p
|
||||
INNER JOIN dbo.ProjectUserAccess pua ON pua.ProjectID = p.ProjectID
|
||||
WHERE p.ProjectID = @ProjectID
|
||||
AND p.IsArchived = 0
|
||||
AND pua.UserID = @UserID
|
||||
AND pua.IsActive = 1
|
||||
)
|
||||
RETURN;
|
||||
|
||||
DECLARE @NextBookNumber int = ISNULL((SELECT MAX(BookNumber) FROM dbo.Books WHERE ProjectID = @ProjectID), 0) + 1;
|
||||
DECLARE @NextBookOrder int = ISNULL((SELECT MAX(SortOrder) FROM dbo.Books WHERE ProjectID = @ProjectID), 0) + 10;
|
||||
DECLARE @BookID int;
|
||||
|
||||
INSERT dbo.Books (ProjectID, BookTitle, Status, BookNumber, SortOrder, UsesExplicitScenes)
|
||||
VALUES (@ProjectID, @TrimmedTitle, N'Planning', @NextBookNumber, @NextBookOrder, 0);
|
||||
|
||||
SET @BookID = CAST(SCOPE_IDENTITY() AS int);
|
||||
|
||||
SELECT @BookID AS BookId,
|
||||
@TrimmedTitle AS Title,
|
||||
CAST(NULL AS nvarchar(200)) AS Subtitle,
|
||||
@NextBookOrder AS SortOrder,
|
||||
CAST(0 AS bit) AS UsesExplicitScenes,
|
||||
N'Book created.' AS Message;
|
||||
END;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.WordCompanion_Manuscript_Analyse
|
||||
@UserID int,
|
||||
@ProjectID int,
|
||||
@BookID int,
|
||||
@DocumentGuid uniqueidentifier = NULL,
|
||||
@ChapterCount int,
|
||||
@SceneCount int,
|
||||
@WordCount int
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
DECLARE @ProjectTitle nvarchar(200);
|
||||
DECLARE @BookTitle nvarchar(200);
|
||||
DECLARE @BookSubtitle nvarchar(200);
|
||||
DECLARE @UsesExplicitScenes bit;
|
||||
|
||||
SELECT @ProjectTitle = p.ProjectName,
|
||||
@BookTitle = b.BookTitle,
|
||||
@BookSubtitle = b.Subtitle,
|
||||
@UsesExplicitScenes = b.UsesExplicitScenes
|
||||
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.ProjectID = @ProjectID
|
||||
AND b.IsArchived = 0
|
||||
AND p.IsArchived = 0
|
||||
AND pua.UserID = @UserID
|
||||
AND pua.IsActive = 1;
|
||||
|
||||
IF @BookTitle IS NULL
|
||||
RETURN;
|
||||
|
||||
DECLARE @ExistingChapterCount int = (SELECT COUNT(*) FROM dbo.Chapters WHERE BookID = @BookID AND IsArchived = 0);
|
||||
DECLARE @ExistingSceneCount int =
|
||||
(
|
||||
SELECT COUNT(*)
|
||||
FROM dbo.Scenes s
|
||||
INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID
|
||||
WHERE c.BookID = @BookID
|
||||
AND c.IsArchived = 0
|
||||
AND s.IsArchived = 0
|
||||
);
|
||||
DECLARE @LinkedChapterCount int =
|
||||
(
|
||||
SELECT COUNT(*)
|
||||
FROM dbo.Chapters
|
||||
WHERE BookID = @BookID
|
||||
AND IsArchived = 0
|
||||
AND IsLinkedToManuscript = 1
|
||||
);
|
||||
|
||||
SELECT @ProjectID AS ProjectId,
|
||||
@BookID AS BookId,
|
||||
@ProjectTitle AS ProjectTitle,
|
||||
@BookTitle AS BookTitle,
|
||||
@BookSubtitle AS BookSubtitle,
|
||||
@UsesExplicitScenes AS UsesExplicitScenes,
|
||||
@ChapterCount AS ChapterCount,
|
||||
@SceneCount AS SceneCount,
|
||||
@WordCount AS WordCount,
|
||||
@ExistingChapterCount AS ExistingChapterCount,
|
||||
@ExistingSceneCount AS ExistingSceneCount,
|
||||
@LinkedChapterCount AS LinkedChapterCount,
|
||||
CAST(CASE WHEN @LinkedChapterCount = 0 AND @ChapterCount > 0 THEN 1 ELSE 0 END AS bit) AS CanImport,
|
||||
CAST(CASE WHEN @LinkedChapterCount = 0 AND @ExistingChapterCount > 0 THEN 1 ELSE 0 END AS bit) AS RequiresReplaceConfirmation,
|
||||
CASE
|
||||
WHEN @LinkedChapterCount > 0 THEN N'This Book already contains a linked manuscript.'
|
||||
WHEN @ChapterCount = 0 THEN N'No chapters were found. Use Heading 1 for chapter titles.'
|
||||
WHEN @ExistingChapterCount > 0 THEN N'This Book already contains planned chapters and scenes.'
|
||||
ELSE N'Ready to import manuscript structure.'
|
||||
END AS Message;
|
||||
END;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.WordCompanion_Manuscript_Import
|
||||
@UserID int,
|
||||
@ProjectID int,
|
||||
@BookID int,
|
||||
@DocumentGuid uniqueidentifier,
|
||||
@BindingVersion int = 1,
|
||||
@ReplacePlanned bit = 0,
|
||||
@StructureJson nvarchar(max)
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
SET XACT_ABORT ON;
|
||||
|
||||
IF @DocumentGuid IS NULL OR @DocumentGuid = '00000000-0000-0000-0000-000000000000'
|
||||
SET @DocumentGuid = NEWID();
|
||||
|
||||
DECLARE @BookTitle nvarchar(200);
|
||||
DECLARE @AccessProjectID int;
|
||||
|
||||
SELECT @AccessProjectID = b.ProjectID,
|
||||
@BookTitle = b.BookTitle
|
||||
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.ProjectID = @ProjectID
|
||||
AND b.IsArchived = 0
|
||||
AND p.IsArchived = 0
|
||||
AND pua.UserID = @UserID
|
||||
AND pua.IsActive = 1;
|
||||
|
||||
IF @BookTitle IS NULL
|
||||
RETURN;
|
||||
|
||||
DECLARE @LinkedChapterCount int =
|
||||
(
|
||||
SELECT COUNT(*)
|
||||
FROM dbo.Chapters
|
||||
WHERE BookID = @BookID
|
||||
AND IsArchived = 0
|
||||
AND IsLinkedToManuscript = 1
|
||||
);
|
||||
|
||||
IF @LinkedChapterCount > 0
|
||||
BEGIN
|
||||
SELECT CAST(0 AS bit) AS Imported,
|
||||
CAST(0 AS bit) AS RequiresReplaceConfirmation,
|
||||
N'This Book already contains a linked manuscript.' AS Message,
|
||||
@ProjectID AS ProjectId,
|
||||
@BookID AS BookId,
|
||||
0 AS ChaptersCreated,
|
||||
0 AS ScenesCreated,
|
||||
0 AS ArchivedChapters,
|
||||
0 AS ArchivedScenes;
|
||||
RETURN;
|
||||
END;
|
||||
|
||||
DECLARE @ChapterCount int = (SELECT COUNT(*) FROM dbo.Chapters WHERE BookID = @BookID AND IsArchived = 0);
|
||||
IF @ChapterCount > 0 AND @ReplacePlanned = 0
|
||||
BEGIN
|
||||
SELECT CAST(0 AS bit) AS Imported,
|
||||
CAST(1 AS bit) AS RequiresReplaceConfirmation,
|
||||
N'This Book already contains planned chapters and scenes.' AS Message,
|
||||
@ProjectID AS ProjectId,
|
||||
@BookID AS BookId,
|
||||
0 AS ChaptersCreated,
|
||||
0 AS ScenesCreated,
|
||||
0 AS ArchivedChapters,
|
||||
0 AS ArchivedScenes;
|
||||
RETURN;
|
||||
END;
|
||||
|
||||
DECLARE @Chapters table
|
||||
(
|
||||
ImportChapterIndex int NOT NULL PRIMARY KEY,
|
||||
Title nvarchar(200) NOT NULL,
|
||||
SortOrder int NOT NULL,
|
||||
WordCount int NOT NULL
|
||||
);
|
||||
|
||||
DECLARE @Scenes table
|
||||
(
|
||||
ImportChapterIndex int NOT NULL,
|
||||
ImportSceneIndex int NOT NULL,
|
||||
Title nvarchar(200) NOT NULL,
|
||||
SortOrder int NOT NULL,
|
||||
WordCount int NOT NULL,
|
||||
PRIMARY KEY (ImportChapterIndex, ImportSceneIndex)
|
||||
);
|
||||
|
||||
INSERT @Chapters (ImportChapterIndex, Title, SortOrder, WordCount)
|
||||
SELECT CONVERT(int, chapter.[key]) + 1,
|
||||
LEFT(LTRIM(RTRIM(JSON_VALUE(chapter.value, '$.Title'))), 200),
|
||||
TRY_CONVERT(int, JSON_VALUE(chapter.value, '$.SortOrder')),
|
||||
COALESCE(TRY_CONVERT(int, JSON_VALUE(chapter.value, '$.WordCount')), 0)
|
||||
FROM OPENJSON(@StructureJson) chapter
|
||||
WHERE NULLIF(LTRIM(RTRIM(JSON_VALUE(chapter.value, '$.Title'))), N'') IS NOT NULL;
|
||||
|
||||
INSERT @Scenes (ImportChapterIndex, ImportSceneIndex, Title, SortOrder, WordCount)
|
||||
SELECT c.ImportChapterIndex,
|
||||
CONVERT(int, scene.[key]) + 1,
|
||||
LEFT(LTRIM(RTRIM(JSON_VALUE(scene.value, '$.Title'))), 200),
|
||||
TRY_CONVERT(int, JSON_VALUE(scene.value, '$.SortOrder')),
|
||||
COALESCE(TRY_CONVERT(int, JSON_VALUE(scene.value, '$.WordCount')), 0)
|
||||
FROM @Chapters c
|
||||
INNER JOIN OPENJSON(@StructureJson) chapter ON CONVERT(int, chapter.[key]) + 1 = c.ImportChapterIndex
|
||||
CROSS APPLY OPENJSON(chapter.value, '$.Scenes') scene
|
||||
WHERE NULLIF(LTRIM(RTRIM(JSON_VALUE(scene.value, '$.Title'))), N'') IS NOT NULL;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM @Chapters)
|
||||
RETURN;
|
||||
|
||||
IF EXISTS (SELECT 1 FROM @Chapters WHERE SortOrder IS NULL OR SortOrder <= 0 OR WordCount < 0)
|
||||
RETURN;
|
||||
|
||||
IF EXISTS (SELECT 1 FROM @Scenes WHERE SortOrder IS NULL OR SortOrder <= 0 OR WordCount < 0)
|
||||
RETURN;
|
||||
|
||||
IF EXISTS (SELECT 1 FROM @Chapters c WHERE NOT EXISTS (SELECT 1 FROM @Scenes s WHERE s.ImportChapterIndex = c.ImportChapterIndex))
|
||||
RETURN;
|
||||
|
||||
DECLARE @PlannedRevisionStatusID int = (SELECT TOP (1) RevisionStatusID FROM dbo.RevisionStatuses WHERE StatusName = N'Planned' ORDER BY SortOrder);
|
||||
DECLARE @DefaultTimeModeID int = (SELECT TOP (1) TimeModeID FROM dbo.TimeModes ORDER BY CASE WHEN TimeModeName = N'Unknown' THEN 0 ELSE 1 END, SortOrder, TimeModeID);
|
||||
DECLARE @DefaultTimeConfidenceID int = (SELECT TOP (1) TimeConfidenceID FROM dbo.TimeConfidences ORDER BY CASE WHEN TimeConfidenceName = N'Unknown' THEN 0 ELSE 1 END, SortOrder, TimeConfidenceID);
|
||||
|
||||
IF @PlannedRevisionStatusID IS NULL OR @DefaultTimeModeID IS NULL OR @DefaultTimeConfidenceID IS NULL
|
||||
RETURN;
|
||||
|
||||
DECLARE @ImportChapterMap table
|
||||
(
|
||||
ImportChapterIndex int NOT NULL PRIMARY KEY,
|
||||
ChapterID int NOT NULL
|
||||
);
|
||||
|
||||
DECLARE @ImportSceneMap table
|
||||
(
|
||||
ImportChapterIndex int NOT NULL,
|
||||
ImportSceneIndex int NOT NULL,
|
||||
SceneID int NOT NULL,
|
||||
PRIMARY KEY (ImportChapterIndex, ImportSceneIndex)
|
||||
);
|
||||
|
||||
DECLARE @ArchivedChapters int = 0;
|
||||
DECLARE @ArchivedScenes int = 0;
|
||||
DECLARE @ManuscriptDocumentID int;
|
||||
DECLARE @LinkedUtc datetime2 = SYSUTCDATETIME();
|
||||
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
IF @ChapterCount > 0
|
||||
BEGIN
|
||||
UPDATE s
|
||||
SET IsArchived = 1,
|
||||
ArchivedDate = SYSUTCDATETIME(),
|
||||
ArchivedReason = N'Replaced by Word Companion manuscript import',
|
||||
UpdatedDate = SYSUTCDATETIME()
|
||||
FROM dbo.Scenes s
|
||||
INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID
|
||||
WHERE c.BookID = @BookID
|
||||
AND c.IsArchived = 0
|
||||
AND s.IsArchived = 0
|
||||
AND c.IsLinkedToManuscript = 0
|
||||
AND s.IsLinkedToManuscript = 0;
|
||||
|
||||
SET @ArchivedScenes = @@ROWCOUNT;
|
||||
|
||||
UPDATE dbo.Chapters
|
||||
SET IsArchived = 1,
|
||||
ArchivedDate = SYSUTCDATETIME(),
|
||||
ArchivedReason = N'Replaced by Word Companion manuscript import',
|
||||
UpdatedDate = SYSUTCDATETIME()
|
||||
WHERE BookID = @BookID
|
||||
AND IsArchived = 0
|
||||
AND IsLinkedToManuscript = 0;
|
||||
|
||||
SET @ArchivedChapters = @@ROWCOUNT;
|
||||
END;
|
||||
|
||||
IF EXISTS
|
||||
(
|
||||
SELECT 1
|
||||
FROM dbo.ManuscriptDocuments
|
||||
WHERE DocumentGuid = @DocumentGuid
|
||||
AND IsActive = 1
|
||||
AND BookID <> @BookID
|
||||
)
|
||||
THROW 51000, 'This manuscript document is already linked to another book.', 1;
|
||||
|
||||
SELECT @ManuscriptDocumentID = ManuscriptDocumentID
|
||||
FROM dbo.ManuscriptDocuments
|
||||
WHERE BookID = @BookID
|
||||
AND IsActive = 1;
|
||||
|
||||
IF @ManuscriptDocumentID IS NULL
|
||||
BEGIN
|
||||
INSERT dbo.ManuscriptDocuments (BookID, ProjectID, DocumentGuid, BindingVersion, CreatedByUserID, LastOpenedUtc, LastSyncUtc)
|
||||
VALUES (@BookID, @ProjectID, @DocumentGuid, @BindingVersion, @UserID, @LinkedUtc, @LinkedUtc);
|
||||
|
||||
SET @ManuscriptDocumentID = CAST(SCOPE_IDENTITY() AS int);
|
||||
END
|
||||
ELSE
|
||||
BEGIN
|
||||
UPDATE dbo.ManuscriptDocuments
|
||||
SET DocumentGuid = @DocumentGuid,
|
||||
ProjectID = @ProjectID,
|
||||
BindingVersion = @BindingVersion,
|
||||
LastOpenedUtc = @LinkedUtc,
|
||||
LastSyncUtc = @LinkedUtc
|
||||
WHERE ManuscriptDocumentID = @ManuscriptDocumentID;
|
||||
END;
|
||||
|
||||
DECLARE @ImportChapterIndex int;
|
||||
DECLARE @ChapterTitle nvarchar(200);
|
||||
DECLARE @ChapterSortOrder int;
|
||||
DECLARE @ChapterID int;
|
||||
DECLARE @ChapterNumber decimal(9,2);
|
||||
|
||||
DECLARE chapter_cursor CURSOR LOCAL FAST_FORWARD FOR
|
||||
SELECT ImportChapterIndex, Title, SortOrder
|
||||
FROM @Chapters
|
||||
ORDER BY SortOrder, ImportChapterIndex;
|
||||
|
||||
OPEN chapter_cursor;
|
||||
FETCH NEXT FROM chapter_cursor INTO @ImportChapterIndex, @ChapterTitle, @ChapterSortOrder;
|
||||
|
||||
WHILE @@FETCH_STATUS = 0
|
||||
BEGIN
|
||||
SET @ChapterNumber = @ImportChapterIndex;
|
||||
|
||||
INSERT dbo.Chapters
|
||||
(
|
||||
BookID, ChapterNumber, ChapterTitle, Summary, SortOrder, RevisionStatusID,
|
||||
IsLinkedToManuscript, ManuscriptDocumentID, ExternalReference, LinkedUtc
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
@BookID, @ChapterNumber, @ChapterTitle, NULL, @ChapterSortOrder, @PlannedRevisionStatusID,
|
||||
1, @ManuscriptDocumentID, CONCAT(N'PD_CHAPTER_IMPORT_', @ImportChapterIndex), @LinkedUtc
|
||||
);
|
||||
|
||||
SET @ChapterID = CAST(SCOPE_IDENTITY() AS int);
|
||||
INSERT @ImportChapterMap (ImportChapterIndex, ChapterID) VALUES (@ImportChapterIndex, @ChapterID);
|
||||
|
||||
FETCH NEXT FROM chapter_cursor INTO @ImportChapterIndex, @ChapterTitle, @ChapterSortOrder;
|
||||
END;
|
||||
|
||||
CLOSE chapter_cursor;
|
||||
DEALLOCATE chapter_cursor;
|
||||
|
||||
DECLARE @ImportSceneIndex int;
|
||||
DECLARE @SceneTitle nvarchar(200);
|
||||
DECLARE @SceneSortOrder int;
|
||||
DECLARE @SceneWordCount int;
|
||||
DECLARE @SceneID int;
|
||||
|
||||
DECLARE scene_cursor CURSOR LOCAL FAST_FORWARD FOR
|
||||
SELECT s.ImportChapterIndex, s.ImportSceneIndex, s.Title, s.SortOrder, s.WordCount, m.ChapterID
|
||||
FROM @Scenes s
|
||||
INNER JOIN @ImportChapterMap m ON m.ImportChapterIndex = s.ImportChapterIndex
|
||||
ORDER BY s.ImportChapterIndex, s.SortOrder, s.ImportSceneIndex;
|
||||
|
||||
OPEN scene_cursor;
|
||||
FETCH NEXT FROM scene_cursor INTO @ImportChapterIndex, @ImportSceneIndex, @SceneTitle, @SceneSortOrder, @SceneWordCount, @ChapterID;
|
||||
|
||||
WHILE @@FETCH_STATUS = 0
|
||||
BEGIN
|
||||
IF @ImportSceneIndex = 1
|
||||
BEGIN
|
||||
SELECT TOP (1) @SceneID = SceneID
|
||||
FROM dbo.Scenes
|
||||
WHERE ChapterID = @ChapterID
|
||||
AND IsArchived = 0
|
||||
ORDER BY SortOrder, SceneID;
|
||||
|
||||
UPDATE dbo.Scenes
|
||||
SET SceneNumber = @ImportSceneIndex,
|
||||
SceneTitle = @SceneTitle,
|
||||
SortOrder = @SceneSortOrder,
|
||||
IsLinkedToManuscript = 1,
|
||||
ManuscriptDocumentID = @ManuscriptDocumentID,
|
||||
ExternalReference = CONCAT(N'PD_SCENE_IMPORT_', @ImportChapterIndex, N'_', @ImportSceneIndex),
|
||||
LinkedUtc = @LinkedUtc,
|
||||
UpdatedDate = SYSUTCDATETIME()
|
||||
WHERE SceneID = @SceneID;
|
||||
END
|
||||
ELSE
|
||||
BEGIN
|
||||
INSERT dbo.Scenes
|
||||
(
|
||||
ChapterID, SceneNumber, SceneTitle, Summary, SortOrder, TimeModeID, TimeConfidenceID, RevisionStatusID,
|
||||
IsLinkedToManuscript, ManuscriptDocumentID, ExternalReference, LinkedUtc
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
@ChapterID, @ImportSceneIndex, @SceneTitle, NULL, @SceneSortOrder, @DefaultTimeModeID, @DefaultTimeConfidenceID, @PlannedRevisionStatusID,
|
||||
1, @ManuscriptDocumentID, CONCAT(N'PD_SCENE_IMPORT_', @ImportChapterIndex, N'_', @ImportSceneIndex), @LinkedUtc
|
||||
);
|
||||
|
||||
SET @SceneID = CAST(SCOPE_IDENTITY() AS int);
|
||||
END;
|
||||
|
||||
IF EXISTS (SELECT 1 FROM dbo.SceneWorkflow WHERE SceneID = @SceneID)
|
||||
BEGIN
|
||||
UPDATE dbo.SceneWorkflow
|
||||
SET ActualWordCount = @SceneWordCount,
|
||||
UpdatedDate = SYSUTCDATETIME()
|
||||
WHERE SceneID = @SceneID;
|
||||
END
|
||||
ELSE
|
||||
BEGIN
|
||||
INSERT dbo.SceneWorkflow (SceneID, ActualWordCount)
|
||||
VALUES (@SceneID, @SceneWordCount);
|
||||
END;
|
||||
|
||||
INSERT @ImportSceneMap (ImportChapterIndex, ImportSceneIndex, SceneID)
|
||||
VALUES (@ImportChapterIndex, @ImportSceneIndex, @SceneID);
|
||||
|
||||
FETCH NEXT FROM scene_cursor INTO @ImportChapterIndex, @ImportSceneIndex, @SceneTitle, @SceneSortOrder, @SceneWordCount, @ChapterID;
|
||||
END;
|
||||
|
||||
CLOSE scene_cursor;
|
||||
DEALLOCATE scene_cursor;
|
||||
|
||||
COMMIT TRANSACTION;
|
||||
|
||||
SELECT CAST(1 AS bit) AS Imported,
|
||||
CAST(0 AS bit) AS RequiresReplaceConfirmation,
|
||||
N'Manuscript structure imported.' AS Message,
|
||||
@ProjectID AS ProjectId,
|
||||
@BookID AS BookId,
|
||||
(SELECT COUNT(*) FROM @ImportChapterMap) AS ChaptersCreated,
|
||||
(SELECT COUNT(*) FROM @ImportSceneMap) AS ScenesCreated,
|
||||
@ArchivedChapters AS ArchivedChapters,
|
||||
@ArchivedScenes AS ArchivedScenes;
|
||||
|
||||
SELECT md.ManuscriptDocumentID AS ManuscriptDocumentId,
|
||||
md.BookID AS BookId,
|
||||
md.ProjectID AS ProjectId,
|
||||
md.DocumentGuid,
|
||||
md.BindingVersion,
|
||||
md.CreatedUtc,
|
||||
md.LastSyncUtc,
|
||||
md.LastOpenedUtc,
|
||||
md.IsActive
|
||||
FROM dbo.ManuscriptDocuments md
|
||||
WHERE md.ManuscriptDocumentID = @ManuscriptDocumentID;
|
||||
END;
|
||||
GO
|
||||
@ -42,6 +42,41 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
<section class="word-companion-section word-companion-first-run" data-first-run-wizard hidden aria-label="First-run manuscript import">
|
||||
<div class="word-companion-section-header">
|
||||
<span>Bind Manuscript</span>
|
||||
</div>
|
||||
<div>
|
||||
<label for="word-companion-first-run-project">Project</label>
|
||||
<select id="word-companion-first-run-project" data-first-run-project-select disabled>
|
||||
<option value="">Loading projects...</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="word-companion-first-run-book">Book</label>
|
||||
<select id="word-companion-first-run-book" data-first-run-book-select disabled>
|
||||
<option value="">Select a project first</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="word-companion-inline-create">
|
||||
<label for="word-companion-first-run-book-title">Create new Book</label>
|
||||
<div>
|
||||
<input id="word-companion-first-run-book-title" type="text" maxlength="200" data-first-run-new-book-title autocomplete="off" />
|
||||
<button type="button" data-first-run-create-book disabled>Create</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="word-companion-actions">
|
||||
<button type="button" class="word-companion-primary-action" data-first-run-scan disabled>Scan Manuscript</button>
|
||||
<button type="button" data-first-run-cancel>Cancel</button>
|
||||
</div>
|
||||
<p class="word-companion-message" data-first-run-status>Select a Project and Book to begin.</p>
|
||||
<div class="word-companion-import-summary" data-first-run-summary hidden></div>
|
||||
<div class="word-companion-actions" data-first-run-import-actions hidden>
|
||||
<button type="button" class="word-companion-primary-action" data-first-run-import disabled>Import Structure</button>
|
||||
<button type="button" data-first-run-import-cancel>Cancel</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<nav class="word-companion-tabs" aria-label="Companion sections">
|
||||
<button type="button" class="is-active" data-tab-button="scene" aria-selected="true">Scene</button>
|
||||
<button type="button" data-tab-button="links" aria-selected="false">Links</button>
|
||||
@ -432,6 +467,20 @@
|
||||
</div>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<dialog class="word-companion-dialog" data-first-run-replace-dialog>
|
||||
<form method="dialog">
|
||||
<div class="word-companion-section-header">
|
||||
<span>Replace Planned Structure?</span>
|
||||
</div>
|
||||
<p class="word-companion-dialog-copy">This Book already contains planned chapters and scenes.</p>
|
||||
<p class="word-companion-dialog-copy">Importing this manuscript will replace the planned structure.</p>
|
||||
<div class="word-companion-dialog-actions">
|
||||
<button type="button" data-first-run-replace-confirm>Continue</button>
|
||||
<button type="button" data-first-run-replace-cancel>Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
</dialog>
|
||||
}
|
||||
</main>
|
||||
<script src="~/js/word-companion-host.js" asp-append-version="true"></script>
|
||||
|
||||
@ -321,7 +321,8 @@ body {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.word-companion-section select {
|
||||
.word-companion-section select,
|
||||
.word-companion-section input[type="text"] {
|
||||
width: 100%;
|
||||
min-height: 34px;
|
||||
border: 1px solid var(--companion-line);
|
||||
@ -337,6 +338,67 @@ body {
|
||||
gap: 9px;
|
||||
}
|
||||
|
||||
.word-companion-first-run {
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.word-companion-inline-create {
|
||||
display: grid;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.word-companion-inline-create > div {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.word-companion-inline-create button {
|
||||
min-height: 34px;
|
||||
border: 1px solid var(--companion-accent);
|
||||
border-radius: 6px;
|
||||
padding: 6px 9px;
|
||||
background: var(--companion-accent);
|
||||
color: #fff9ef;
|
||||
font: inherit;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.word-companion-inline-create button:disabled {
|
||||
border-color: var(--companion-line);
|
||||
background: var(--companion-soft);
|
||||
color: var(--companion-muted);
|
||||
}
|
||||
|
||||
.word-companion-import-summary {
|
||||
display: grid;
|
||||
gap: 7px;
|
||||
border-top: 1px solid var(--companion-line);
|
||||
padding-top: 9px;
|
||||
}
|
||||
|
||||
.word-companion-import-summary dl {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
gap: 5px 10px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.word-companion-import-summary dt {
|
||||
color: var(--companion-muted);
|
||||
font-size: 0.76rem;
|
||||
font-weight: 800;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.word-companion-import-summary dd {
|
||||
margin: 0;
|
||||
color: var(--companion-ink);
|
||||
font-weight: 800;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.word-companion-connection .word-companion-message {
|
||||
grid-column: auto;
|
||||
}
|
||||
|
||||
2
PlotLine/wwwroot/css/word-companion.min.css
vendored
2
PlotLine/wwwroot/css/word-companion.min.css
vendored
File diff suppressed because one or more lines are too long
@ -4,11 +4,33 @@
|
||||
bookId: "plotdirector.word.bookId",
|
||||
activeTab: "plotdirector.word.activeTab"
|
||||
};
|
||||
const documentMetadataKeys = {
|
||||
documentGuid: "plotdirector.word.documentGuid",
|
||||
bookId: "plotdirector.word.boundBookId",
|
||||
projectId: "plotdirector.word.boundProjectId",
|
||||
bindingVersion: "plotdirector.word.bindingVersion"
|
||||
};
|
||||
const validTabNames = new Set(["scene", "links", "structure", "diagnostics"]);
|
||||
|
||||
const shell = document.querySelector(".word-companion-shell");
|
||||
const tabsNav = document.querySelector(".word-companion-tabs");
|
||||
const tabButtons = [...document.querySelectorAll("[data-tab-button]")];
|
||||
const tabPanels = [...document.querySelectorAll("[data-tab-panel]")];
|
||||
const firstRunWizard = document.querySelector("[data-first-run-wizard]");
|
||||
const firstRunProjectSelect = document.querySelector("[data-first-run-project-select]");
|
||||
const firstRunBookSelect = document.querySelector("[data-first-run-book-select]");
|
||||
const firstRunNewBookTitle = document.querySelector("[data-first-run-new-book-title]");
|
||||
const firstRunCreateBookButton = document.querySelector("[data-first-run-create-book]");
|
||||
const firstRunScanButton = document.querySelector("[data-first-run-scan]");
|
||||
const firstRunCancelButton = document.querySelector("[data-first-run-cancel]");
|
||||
const firstRunStatus = document.querySelector("[data-first-run-status]");
|
||||
const firstRunSummary = document.querySelector("[data-first-run-summary]");
|
||||
const firstRunImportActions = document.querySelector("[data-first-run-import-actions]");
|
||||
const firstRunImportButton = document.querySelector("[data-first-run-import]");
|
||||
const firstRunImportCancelButton = document.querySelector("[data-first-run-import-cancel]");
|
||||
const firstRunReplaceDialog = document.querySelector("[data-first-run-replace-dialog]");
|
||||
const firstRunReplaceConfirm = document.querySelector("[data-first-run-replace-confirm]");
|
||||
const firstRunReplaceCancel = document.querySelector("[data-first-run-replace-cancel]");
|
||||
const officeStatus = document.querySelector("[data-office-status]");
|
||||
const connectionStatus = document.querySelector("[data-connection-status]");
|
||||
const summaryConnection = document.querySelector("[data-summary-connection]");
|
||||
@ -148,6 +170,10 @@
|
||||
let selectedLinkChapterId = null;
|
||||
let pendingCreateChapterConfirmation = null;
|
||||
let structurePreviewModel = null;
|
||||
let firstRunDocumentBinding = null;
|
||||
let firstRunAnalysis = null;
|
||||
let firstRunImportStructure = null;
|
||||
let isFirstRunImporting = false;
|
||||
let isApplyingStructureSync = false;
|
||||
let pendingApplyStructureSyncConfirmation = null;
|
||||
let sceneTrackingMode = "Manual";
|
||||
@ -933,6 +959,181 @@
|
||||
return books.find((book) => book.bookId === bookId) || null;
|
||||
};
|
||||
|
||||
const selectedFirstRunProject = () => {
|
||||
const projectId = Number.parseInt(firstRunProjectSelect?.value || "", 10);
|
||||
return projects.find((project) => project.projectId === projectId) || null;
|
||||
};
|
||||
|
||||
const selectedFirstRunBook = () => {
|
||||
const bookId = Number.parseInt(firstRunBookSelect?.value || "", 10);
|
||||
return books.find((book) => book.bookId === bookId) || null;
|
||||
};
|
||||
|
||||
const setFirstRunStatus = (message) => setText(firstRunStatus, message);
|
||||
|
||||
const updateFirstRunButtons = () => {
|
||||
const project = selectedFirstRunProject();
|
||||
const book = selectedFirstRunBook();
|
||||
if (firstRunScanButton) {
|
||||
firstRunScanButton.disabled = !project || !book || isFirstRunImporting;
|
||||
}
|
||||
if (firstRunCreateBookButton) {
|
||||
firstRunCreateBookButton.disabled = !project || !String(firstRunNewBookTitle?.value || "").trim() || isFirstRunImporting;
|
||||
}
|
||||
if (firstRunImportButton) {
|
||||
firstRunImportButton.disabled = !firstRunAnalysis?.canImport || !firstRunImportStructure || isFirstRunImporting;
|
||||
}
|
||||
};
|
||||
|
||||
const setFirstRunMode = (visible) => {
|
||||
setHidden(firstRunWizard, !visible);
|
||||
setHidden(tabsNav, visible);
|
||||
for (const panel of tabPanels) {
|
||||
setHidden(panel, visible ? true : panel.dataset.tabPanel !== "scene" && !panel.classList.contains("is-active"));
|
||||
}
|
||||
if (!visible) {
|
||||
restoreActiveTab();
|
||||
}
|
||||
};
|
||||
|
||||
const syncFirstRunProjectOptions = () => {
|
||||
if (!firstRunProjectSelect) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (projects.length === 0) {
|
||||
resetSelect(firstRunProjectSelect, "No projects available.");
|
||||
return;
|
||||
}
|
||||
|
||||
addOptions(firstRunProjectSelect, "Choose a project", projects, "projectId", "title");
|
||||
const currentProjectId = Number.parseInt(projectSelect?.value || "", 10);
|
||||
if (Number.isInteger(currentProjectId) && currentProjectId > 0) {
|
||||
firstRunProjectSelect.value = String(currentProjectId);
|
||||
}
|
||||
};
|
||||
|
||||
const syncFirstRunBookOptions = (preferredBookId = null) => {
|
||||
if (!firstRunBookSelect) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (books.length === 0) {
|
||||
resetSelect(firstRunBookSelect, "No books available.");
|
||||
return;
|
||||
}
|
||||
|
||||
addOptions(firstRunBookSelect, "Choose a book", books.map((book) => ({
|
||||
...book,
|
||||
displayTitle: bookDisplayTitle(book)
|
||||
})), "bookId", "displayTitle");
|
||||
|
||||
const currentBookId = preferredBookId || Number.parseInt(bookSelect?.value || "", 10);
|
||||
const validBook = books.find((book) => book.bookId === currentBookId);
|
||||
if (validBook) {
|
||||
firstRunBookSelect.value = String(validBook.bookId);
|
||||
}
|
||||
updateFirstRunButtons();
|
||||
};
|
||||
|
||||
const resetFirstRunPreview = (message = "Select a Project and Book to begin.") => {
|
||||
firstRunAnalysis = null;
|
||||
firstRunImportStructure = null;
|
||||
setHidden(firstRunSummary, true);
|
||||
setHidden(firstRunImportActions, true);
|
||||
if (firstRunSummary) {
|
||||
firstRunSummary.innerHTML = "";
|
||||
}
|
||||
setFirstRunStatus(message);
|
||||
updateFirstRunButtons();
|
||||
};
|
||||
|
||||
const renderFirstRunSummary = (analysis) => {
|
||||
if (!firstRunSummary) {
|
||||
return;
|
||||
}
|
||||
|
||||
firstRunSummary.innerHTML = "";
|
||||
const title = document.createElement("strong");
|
||||
title.textContent = "Manuscript Analysis";
|
||||
firstRunSummary.append(title);
|
||||
|
||||
const list = document.createElement("dl");
|
||||
const rows = [
|
||||
["Project", analysis.projectTitle],
|
||||
["Book", bookDisplayTitle({ title: analysis.bookTitle, subtitle: analysis.bookSubtitle })],
|
||||
["Chapters", Number(analysis.chapterCount || 0).toLocaleString()],
|
||||
["Scenes", Number(analysis.sceneCount || 0).toLocaleString()],
|
||||
["Words", Number(analysis.wordCount || 0).toLocaleString()]
|
||||
];
|
||||
for (const [label, value] of rows) {
|
||||
const term = document.createElement("dt");
|
||||
term.textContent = label;
|
||||
const description = document.createElement("dd");
|
||||
description.textContent = value || "-";
|
||||
list.append(term, description);
|
||||
}
|
||||
firstRunSummary.append(list);
|
||||
setHidden(firstRunSummary, false);
|
||||
setHidden(firstRunImportActions, false);
|
||||
setFirstRunStatus(analysis.message || "Preview generated.");
|
||||
updateFirstRunButtons();
|
||||
};
|
||||
|
||||
const readDocumentBinding = () => {
|
||||
const settings = window.Office?.context?.document?.settings;
|
||||
if (!settings) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const documentGuid = String(settings.get(documentMetadataKeys.documentGuid) || "").trim();
|
||||
const bookId = Number.parseInt(settings.get(documentMetadataKeys.bookId) || "", 10);
|
||||
const projectId = Number.parseInt(settings.get(documentMetadataKeys.projectId) || "", 10);
|
||||
const bindingVersion = Number.parseInt(settings.get(documentMetadataKeys.bindingVersion) || "", 10);
|
||||
|
||||
if (!documentGuid || !Number.isInteger(bookId) || bookId <= 0 || !Number.isInteger(projectId) || projectId <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
documentGuid,
|
||||
bookId,
|
||||
projectId,
|
||||
bindingVersion: Number.isInteger(bindingVersion) && bindingVersion > 0 ? bindingVersion : 1
|
||||
};
|
||||
};
|
||||
|
||||
const writeDocumentBinding = (binding) => new Promise((resolve, reject) => {
|
||||
const settings = window.Office?.context?.document?.settings;
|
||||
if (!settings) {
|
||||
reject(new Error("Word document settings are unavailable."));
|
||||
return;
|
||||
}
|
||||
|
||||
settings.set(documentMetadataKeys.documentGuid, binding.documentGuid);
|
||||
settings.set(documentMetadataKeys.bookId, String(binding.bookId));
|
||||
settings.set(documentMetadataKeys.projectId, String(binding.projectId));
|
||||
settings.set(documentMetadataKeys.bindingVersion, String(binding.bindingVersion || 1));
|
||||
settings.saveAsync((result) => {
|
||||
if (result.status === window.Office.AsyncResultStatus.Succeeded) {
|
||||
resolve();
|
||||
} else {
|
||||
reject(result.error || new Error("Unable to save Word document metadata."));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const ensureDocumentGuid = () => {
|
||||
if (firstRunDocumentBinding?.documentGuid) {
|
||||
return firstRunDocumentBinding.documentGuid;
|
||||
}
|
||||
if (window.crypto?.randomUUID) {
|
||||
return window.crypto.randomUUID();
|
||||
}
|
||||
return "10000000-1000-4000-8000-100000000000".replace(/[018]/g, (char) =>
|
||||
(Number(char) ^ Math.random() * 16 >> Number(char) / 4).toString(16));
|
||||
};
|
||||
|
||||
const updateSummary = () => {
|
||||
const project = selectedProject();
|
||||
const book = selectedBook();
|
||||
@ -1061,6 +1262,69 @@
|
||||
};
|
||||
};
|
||||
|
||||
const buildFirstRunImportStructure = (paragraphs, usesExplicitScenes) => {
|
||||
const chapters = [];
|
||||
let chapter = null;
|
||||
let scene = null;
|
||||
let totalWords = 0;
|
||||
const separatorTexts = new Set(["***", "###"]);
|
||||
|
||||
const startScene = () => {
|
||||
if (!chapter) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const nextScene = {
|
||||
title: `Scene ${chapter.scenes.length + 1}`,
|
||||
sortOrder: (chapter.scenes.length + 1) * 10,
|
||||
wordCount: 0
|
||||
};
|
||||
chapter.scenes.push(nextScene);
|
||||
return nextScene;
|
||||
};
|
||||
|
||||
paragraphs.forEach((paragraph) => {
|
||||
const text = String(paragraph.text || "").trim();
|
||||
if (!text) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isBuiltInHeading(paragraph, 1)) {
|
||||
chapter = {
|
||||
title: text,
|
||||
sortOrder: (chapters.length + 1) * 10,
|
||||
wordCount: 0,
|
||||
scenes: []
|
||||
};
|
||||
chapters.push(chapter);
|
||||
scene = startScene();
|
||||
totalWords += countWords(text);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!chapter || !scene) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (usesExplicitScenes && separatorTexts.has(text)) {
|
||||
scene = startScene();
|
||||
return;
|
||||
}
|
||||
|
||||
const words = countWords(text);
|
||||
chapter.wordCount += words;
|
||||
scene.wordCount += words;
|
||||
totalWords += words;
|
||||
});
|
||||
|
||||
return {
|
||||
chapters,
|
||||
chapterCount: chapters.length,
|
||||
sceneCount: chapters.reduce((total, item) => total + item.scenes.length, 0),
|
||||
wordCount: totalWords
|
||||
};
|
||||
};
|
||||
|
||||
const renderOutline = (structure) => {
|
||||
if (!documentOutline) {
|
||||
return;
|
||||
@ -1918,6 +2182,204 @@
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
|
||||
const analyseFirstRunManuscript = async () => {
|
||||
const project = selectedFirstRunProject();
|
||||
const book = selectedFirstRunBook();
|
||||
if (!project || !book) {
|
||||
resetFirstRunPreview("Select a Project and Book to begin.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!officeReady || !wordHostAvailable || !window.Word || typeof window.Word.run !== "function") {
|
||||
resetFirstRunPreview("Word document access is unavailable.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (firstRunScanButton) {
|
||||
firstRunScanButton.disabled = true;
|
||||
firstRunScanButton.textContent = "Scanning...";
|
||||
}
|
||||
setFirstRunStatus("Scanning manuscript structure...");
|
||||
|
||||
try {
|
||||
const importStructure = await window.Word.run(async (context) => {
|
||||
const bodyParagraphs = context.document.body.paragraphs;
|
||||
bodyParagraphs.load("text,styleBuiltIn,style");
|
||||
await context.sync();
|
||||
return buildFirstRunImportStructure(bodyParagraphs.items, !!book.usesExplicitScenes);
|
||||
});
|
||||
|
||||
firstRunImportStructure = importStructure;
|
||||
const analysis = await postJson("/api/word-companion/manuscript/analyse", {
|
||||
projectId: project.projectId,
|
||||
bookId: book.bookId,
|
||||
documentGuid: ensureDocumentGuid(),
|
||||
chapterCount: importStructure.chapterCount,
|
||||
sceneCount: importStructure.sceneCount,
|
||||
wordCount: importStructure.wordCount
|
||||
});
|
||||
firstRunAnalysis = analysis;
|
||||
renderFirstRunSummary(analysis);
|
||||
setDiagnostics({
|
||||
lastScan: "Success",
|
||||
lastError: "-",
|
||||
paragraphs: "-",
|
||||
heading1: String(importStructure.chapterCount),
|
||||
heading2: "-"
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Unable to scan manuscript for first-run import.", error);
|
||||
resetFirstRunPreview(`Unable to scan manuscript: ${errorText(error)}`);
|
||||
setDiagnostics({ lastScan: "Failure", lastError: errorText(error) });
|
||||
} finally {
|
||||
if (firstRunScanButton) {
|
||||
firstRunScanButton.textContent = "Scan Manuscript";
|
||||
}
|
||||
updateFirstRunButtons();
|
||||
}
|
||||
};
|
||||
|
||||
const requestReplacePlannedConfirmation = () => new Promise((resolve) => {
|
||||
if (!firstRunReplaceDialog || typeof firstRunReplaceDialog.showModal !== "function") {
|
||||
resolve(window.confirm("This Book already contains planned chapters and scenes.\n\nImporting this manuscript will replace the planned structure.\n\nContinue?"));
|
||||
return;
|
||||
}
|
||||
|
||||
const cleanup = (confirmed) => {
|
||||
firstRunReplaceConfirm?.removeEventListener("click", confirmHandler);
|
||||
firstRunReplaceCancel?.removeEventListener("click", cancelHandler);
|
||||
firstRunReplaceDialog?.removeEventListener("cancel", cancelEventHandler);
|
||||
firstRunReplaceDialog?.close();
|
||||
resolve(confirmed);
|
||||
};
|
||||
const confirmHandler = () => cleanup(true);
|
||||
const cancelHandler = () => cleanup(false);
|
||||
const cancelEventHandler = (event) => {
|
||||
event.preventDefault();
|
||||
cleanup(false);
|
||||
};
|
||||
|
||||
firstRunReplaceConfirm?.addEventListener("click", confirmHandler);
|
||||
firstRunReplaceCancel?.addEventListener("click", cancelHandler);
|
||||
firstRunReplaceDialog?.addEventListener("cancel", cancelEventHandler);
|
||||
firstRunReplaceDialog.showModal();
|
||||
});
|
||||
|
||||
const importFirstRunManuscript = async () => {
|
||||
const project = selectedFirstRunProject();
|
||||
const book = selectedFirstRunBook();
|
||||
if (!project || !book || !firstRunImportStructure || !firstRunAnalysis?.canImport) {
|
||||
updateFirstRunButtons();
|
||||
return;
|
||||
}
|
||||
|
||||
let replacePlanned = false;
|
||||
if (firstRunAnalysis.requiresReplaceConfirmation) {
|
||||
replacePlanned = await requestReplacePlannedConfirmation();
|
||||
if (!replacePlanned) {
|
||||
setFirstRunStatus("Import cancelled.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
isFirstRunImporting = true;
|
||||
updateFirstRunButtons();
|
||||
setFirstRunStatus("Importing manuscript structure...");
|
||||
|
||||
try {
|
||||
const documentGuid = ensureDocumentGuid();
|
||||
const response = await postJson("/api/word-companion/manuscript/import", {
|
||||
projectId: project.projectId,
|
||||
bookId: book.bookId,
|
||||
documentGuid,
|
||||
bindingVersion: 1,
|
||||
replacePlanned,
|
||||
chapters: firstRunImportStructure.chapters
|
||||
});
|
||||
|
||||
if (!response.imported) {
|
||||
firstRunAnalysis = {
|
||||
...firstRunAnalysis,
|
||||
canImport: !response.requiresReplaceConfirmation,
|
||||
requiresReplaceConfirmation: !!response.requiresReplaceConfirmation,
|
||||
message: response.message || firstRunAnalysis.message
|
||||
};
|
||||
setFirstRunStatus(response.message || "Import was not completed.");
|
||||
updateFirstRunButtons();
|
||||
return;
|
||||
}
|
||||
|
||||
const document = response.manuscriptDocument;
|
||||
await writeDocumentBinding({
|
||||
documentGuid: document?.documentGuid || documentGuid,
|
||||
bookId: response.bookId,
|
||||
projectId: response.projectId,
|
||||
bindingVersion: document?.bindingVersion || 1
|
||||
});
|
||||
|
||||
firstRunDocumentBinding = {
|
||||
documentGuid: document?.documentGuid || documentGuid,
|
||||
bookId: response.bookId,
|
||||
projectId: response.projectId,
|
||||
bindingVersion: document?.bindingVersion || 1
|
||||
};
|
||||
writeStoredId(storageKeys.projectId, response.projectId);
|
||||
writeStoredId(storageKeys.bookId, response.bookId);
|
||||
if (projectSelect) {
|
||||
projectSelect.value = String(response.projectId);
|
||||
}
|
||||
await loadBooks(response.projectId, response.bookId);
|
||||
setFirstRunMode(false);
|
||||
setConnectionStatus("Connected");
|
||||
setDocumentMessage(response.message || "Manuscript structure imported.");
|
||||
} catch (error) {
|
||||
console.error("Unable to import manuscript structure.", error);
|
||||
setFirstRunStatus(`Unable to import manuscript: ${errorText(error)}`);
|
||||
} finally {
|
||||
isFirstRunImporting = false;
|
||||
updateFirstRunButtons();
|
||||
}
|
||||
};
|
||||
|
||||
const initialiseFirstRunBinding = async () => {
|
||||
if (!isAuthenticated || !wordHostAvailable) {
|
||||
setFirstRunMode(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const binding = readDocumentBinding();
|
||||
if (binding) {
|
||||
try {
|
||||
const response = await fetchJson(`/api/word-companion/manuscript/book/${binding.bookId}`);
|
||||
const document = response?.manuscriptDocument;
|
||||
if (document && String(document.documentGuid).toLowerCase() === binding.documentGuid.toLowerCase()) {
|
||||
firstRunDocumentBinding = binding;
|
||||
writeStoredId(storageKeys.projectId, binding.projectId);
|
||||
writeStoredId(storageKeys.bookId, binding.bookId);
|
||||
if (projectSelect) {
|
||||
projectSelect.value = String(binding.projectId);
|
||||
}
|
||||
await loadBooks(binding.projectId, binding.bookId);
|
||||
setFirstRunMode(false);
|
||||
setConnectionStatus("Connected");
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
// If the saved binding no longer resolves, treat the document as unbound.
|
||||
}
|
||||
}
|
||||
|
||||
firstRunDocumentBinding = null;
|
||||
syncFirstRunProjectOptions();
|
||||
if (selectedFirstRunProject()) {
|
||||
await loadBooks(selectedFirstRunProject().projectId, null);
|
||||
} else {
|
||||
resetSelect(firstRunBookSelect, "Select a project first");
|
||||
}
|
||||
resetFirstRunPreview("Select a Project and Book to begin.");
|
||||
setFirstRunMode(true);
|
||||
};
|
||||
|
||||
const patchJson = (url, body) => fetchJson(url, {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
@ -3079,6 +3541,7 @@
|
||||
if (!projectId) {
|
||||
books = [];
|
||||
resetSelect(bookSelect, "Select a project first");
|
||||
resetSelect(firstRunBookSelect, "Select a project first");
|
||||
setBookMessage("");
|
||||
writeStoredId(storageKeys.bookId, null);
|
||||
updateSummary();
|
||||
@ -3098,9 +3561,11 @@
|
||||
|
||||
if (books.length === 0) {
|
||||
resetSelect(bookSelect, "No books available.");
|
||||
resetSelect(firstRunBookSelect, "No books available.");
|
||||
setBookMessage("No books available.");
|
||||
writeStoredId(storageKeys.bookId, null);
|
||||
updateSummary();
|
||||
resetFirstRunPreview("No books available.");
|
||||
return;
|
||||
}
|
||||
|
||||
@ -3115,25 +3580,33 @@
|
||||
} else {
|
||||
writeStoredId(storageKeys.bookId, null);
|
||||
}
|
||||
syncFirstRunBookOptions(validBook?.bookId || preferredBookId);
|
||||
updateSummary();
|
||||
resetPlotDirectorScene(selectedBook() ? "Not detected" : "Not detected");
|
||||
resetStructurePreview(selectedBook()
|
||||
? "Ready to analyse manuscript structure."
|
||||
: "Select a book to analyse manuscript structure.");
|
||||
resetFirstRunPreview(selectedFirstRunBook()
|
||||
? "Ready to scan manuscript structure."
|
||||
: "Select a Book to begin.");
|
||||
} catch {
|
||||
books = [];
|
||||
resetSelect(bookSelect, "Unable to load books.");
|
||||
resetSelect(firstRunBookSelect, "Unable to load books.");
|
||||
setBookMessage("Unable to load books.");
|
||||
writeStoredId(storageKeys.bookId, null);
|
||||
updateSummary();
|
||||
resetPlotDirectorScene("Not detected");
|
||||
resetStructurePreview("Unable to load books.");
|
||||
resetFirstRunPreview("Unable to load books.");
|
||||
}
|
||||
};
|
||||
|
||||
const loadProjects = async () => {
|
||||
resetSelect(projectSelect, "Loading projects...");
|
||||
resetSelect(bookSelect, "Select a project first");
|
||||
resetSelect(firstRunProjectSelect, "Loading projects...");
|
||||
resetSelect(firstRunBookSelect, "Select a project first");
|
||||
setProjectMessage("");
|
||||
setBookMessage("");
|
||||
|
||||
@ -3143,35 +3616,45 @@
|
||||
|
||||
if (projects.length === 0) {
|
||||
resetSelect(projectSelect, "No projects available.");
|
||||
resetSelect(firstRunProjectSelect, "No projects available.");
|
||||
setProjectMessage("No projects available.");
|
||||
writeStoredId(storageKeys.projectId, null);
|
||||
writeStoredId(storageKeys.bookId, null);
|
||||
updateSummary();
|
||||
resetFirstRunPreview("No projects available.");
|
||||
return;
|
||||
}
|
||||
|
||||
addOptions(projectSelect, "Choose a project", projects, "projectId", "title");
|
||||
syncFirstRunProjectOptions();
|
||||
const storedProjectId = readStoredId(storageKeys.projectId);
|
||||
const validProject = projects.find((project) => project.projectId === storedProjectId);
|
||||
if (validProject && projectSelect) {
|
||||
projectSelect.value = String(validProject.projectId);
|
||||
if (firstRunProjectSelect) {
|
||||
firstRunProjectSelect.value = String(validProject.projectId);
|
||||
}
|
||||
writeStoredId(storageKeys.projectId, validProject.projectId);
|
||||
await loadBooks(validProject.projectId, readStoredId(storageKeys.bookId));
|
||||
} else {
|
||||
writeStoredId(storageKeys.projectId, null);
|
||||
writeStoredId(storageKeys.bookId, null);
|
||||
updateSummary();
|
||||
resetFirstRunPreview("Select a Project and Book to begin.");
|
||||
}
|
||||
} catch {
|
||||
projects = [];
|
||||
books = [];
|
||||
resetSelect(projectSelect, "Unable to connect to PlotDirector.");
|
||||
resetSelect(bookSelect, "Select a project first");
|
||||
resetSelect(firstRunProjectSelect, "Unable to connect to PlotDirector.");
|
||||
resetSelect(firstRunBookSelect, "Select a project first");
|
||||
setConnectionStatus("Unable to connect to PlotDirector.");
|
||||
setProjectMessage("Unable to connect to PlotDirector.");
|
||||
writeStoredId(storageKeys.projectId, null);
|
||||
writeStoredId(storageKeys.bookId, null);
|
||||
updateSummary();
|
||||
resetFirstRunPreview("Unable to connect to PlotDirector.");
|
||||
}
|
||||
};
|
||||
|
||||
@ -3182,23 +3665,94 @@
|
||||
|
||||
projectSelect?.addEventListener("change", async () => {
|
||||
const projectId = Number.parseInt(projectSelect.value || "", 10);
|
||||
if (firstRunProjectSelect && Number.isInteger(projectId) && projectId > 0) {
|
||||
firstRunProjectSelect.value = String(projectId);
|
||||
}
|
||||
writeStoredId(storageKeys.projectId, Number.isInteger(projectId) && projectId > 0 ? projectId : null);
|
||||
writeStoredId(storageKeys.bookId, null);
|
||||
resetPlotDirectorScene("Not detected");
|
||||
resetStructurePreview();
|
||||
resetFirstRunPreview("Select a Book to begin.");
|
||||
await loadBooks(projectId, null);
|
||||
});
|
||||
|
||||
bookSelect?.addEventListener("change", () => {
|
||||
const bookId = Number.parseInt(bookSelect.value || "", 10);
|
||||
writeStoredId(storageKeys.bookId, Number.isInteger(bookId) && bookId > 0 ? bookId : null);
|
||||
if (firstRunBookSelect && Number.isInteger(bookId) && bookId > 0) {
|
||||
firstRunBookSelect.value = String(bookId);
|
||||
}
|
||||
updateSummary();
|
||||
resetPlotDirectorScene("Not detected");
|
||||
resetStructurePreview(selectedBook()
|
||||
? "Ready to analyse manuscript structure."
|
||||
: "Select a book to analyse manuscript structure.");
|
||||
resetFirstRunPreview(selectedFirstRunBook()
|
||||
? "Ready to scan manuscript structure."
|
||||
: "Select a Book to begin.");
|
||||
});
|
||||
|
||||
firstRunProjectSelect?.addEventListener("change", async () => {
|
||||
const projectId = Number.parseInt(firstRunProjectSelect.value || "", 10);
|
||||
if (projectSelect && Number.isInteger(projectId) && projectId > 0) {
|
||||
projectSelect.value = String(projectId);
|
||||
}
|
||||
writeStoredId(storageKeys.projectId, Number.isInteger(projectId) && projectId > 0 ? projectId : null);
|
||||
writeStoredId(storageKeys.bookId, null);
|
||||
resetFirstRunPreview("Select a Book to begin.");
|
||||
await loadBooks(projectId, null);
|
||||
});
|
||||
|
||||
firstRunBookSelect?.addEventListener("change", () => {
|
||||
const bookId = Number.parseInt(firstRunBookSelect.value || "", 10);
|
||||
if (bookSelect && Number.isInteger(bookId) && bookId > 0) {
|
||||
bookSelect.value = String(bookId);
|
||||
}
|
||||
writeStoredId(storageKeys.bookId, Number.isInteger(bookId) && bookId > 0 ? bookId : null);
|
||||
updateSummary();
|
||||
resetFirstRunPreview(selectedFirstRunBook()
|
||||
? "Ready to scan manuscript structure."
|
||||
: "Select a Book to begin.");
|
||||
});
|
||||
|
||||
firstRunNewBookTitle?.addEventListener("input", updateFirstRunButtons);
|
||||
|
||||
firstRunCreateBookButton?.addEventListener("click", async () => {
|
||||
const project = selectedFirstRunProject();
|
||||
const title = String(firstRunNewBookTitle?.value || "").trim();
|
||||
if (!project || !title) {
|
||||
updateFirstRunButtons();
|
||||
return;
|
||||
}
|
||||
|
||||
firstRunCreateBookButton.disabled = true;
|
||||
setFirstRunStatus("Creating Book...");
|
||||
try {
|
||||
const response = await postJson(`/api/word-companion/projects/${project.projectId}/books`, { title });
|
||||
if (firstRunNewBookTitle) {
|
||||
firstRunNewBookTitle.value = "";
|
||||
}
|
||||
if (projectSelect) {
|
||||
projectSelect.value = String(project.projectId);
|
||||
}
|
||||
await loadBooks(project.projectId, response.bookId);
|
||||
if (firstRunBookSelect) {
|
||||
firstRunBookSelect.value = String(response.bookId);
|
||||
}
|
||||
setFirstRunStatus("Ready to scan manuscript structure.");
|
||||
} catch (error) {
|
||||
console.error("Unable to create Book from Word Companion.", error);
|
||||
setFirstRunStatus(`Unable to create Book: ${errorText(error)}`);
|
||||
} finally {
|
||||
updateFirstRunButtons();
|
||||
}
|
||||
});
|
||||
|
||||
firstRunScanButton?.addEventListener("click", analyseFirstRunManuscript);
|
||||
firstRunImportButton?.addEventListener("click", importFirstRunManuscript);
|
||||
firstRunCancelButton?.addEventListener("click", () => setFirstRunMode(false));
|
||||
firstRunImportCancelButton?.addEventListener("click", () => resetFirstRunPreview("Import cancelled."));
|
||||
|
||||
refreshDocumentButton?.addEventListener("click", refreshDocumentStructure);
|
||||
analyseManuscriptStructureButton?.addEventListener("click", analyseManuscriptStructure);
|
||||
applyStructureSyncButton?.addEventListener("click", applyStructureSync);
|
||||
@ -3237,11 +3791,12 @@
|
||||
});
|
||||
runDiagnosticsButton?.addEventListener("click", runDiagnostics);
|
||||
|
||||
if (isAuthenticated) {
|
||||
loadProjects();
|
||||
} else {
|
||||
const projectsLoadPromise = isAuthenticated ? loadProjects() : Promise.resolve();
|
||||
if (!isAuthenticated) {
|
||||
resetSelect(projectSelect, "Please sign in to PlotDirector.");
|
||||
resetSelect(bookSelect, "Please sign in to PlotDirector.");
|
||||
resetSelect(firstRunProjectSelect, "Please sign in to PlotDirector.");
|
||||
resetSelect(firstRunBookSelect, "Please sign in to PlotDirector.");
|
||||
updateSummary();
|
||||
}
|
||||
|
||||
@ -3259,8 +3814,10 @@
|
||||
}
|
||||
|
||||
updateOfficeReadiness()
|
||||
.then(() => {
|
||||
.then(async () => {
|
||||
setOfficeStatus("Word Companion ready");
|
||||
await projectsLoadPromise;
|
||||
await initialiseFirstRunBinding();
|
||||
if (!wordHostAvailable) {
|
||||
setDocumentMessage("Word document access is unavailable.");
|
||||
setSceneTrackingState("Manual");
|
||||
|
||||
File diff suppressed because one or more lines are too long
Loading…
x
Reference in New Issue
Block a user