Phase 20F Build the approved manuscript structure into a real PlotDirector project.
This commit is contained in:
parent
f78e1ac8fc
commit
16ea855a3e
@ -24,6 +24,13 @@ public sealed class OnboardingController(IOnboardingService onboarding) : Contro
|
||||
return model is null ? NotFound() : View(model);
|
||||
}
|
||||
|
||||
[HttpGet("build-complete")]
|
||||
public async Task<IActionResult> BuildComplete(Guid previewId)
|
||||
{
|
||||
var model = await onboarding.GetBuildResultAsync(previewId);
|
||||
return model is null ? NotFound() : View(model);
|
||||
}
|
||||
|
||||
[HttpPost("scan-review")]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> SaveScanReview(ManuscriptScanReviewForm form, string intent = "save")
|
||||
|
||||
99
PlotLine/Data/OnboardingBuildRepository.cs
Normal file
99
PlotLine/Data/OnboardingBuildRepository.cs
Normal file
@ -0,0 +1,99 @@
|
||||
using System.Data;
|
||||
using System.Text.Json;
|
||||
using Dapper;
|
||||
using PlotLine.Models;
|
||||
|
||||
namespace PlotLine.Data;
|
||||
|
||||
public interface IOnboardingBuildRepository
|
||||
{
|
||||
Task<OnboardingManuscriptBuildResult?> BuildAsync(int userId, OnboardingManuscriptBuildRequest request);
|
||||
}
|
||||
|
||||
public sealed class OnboardingBuildRepository(ISqlConnectionFactory connectionFactory) : IOnboardingBuildRepository
|
||||
{
|
||||
public async Task<OnboardingManuscriptBuildResult?> BuildAsync(int userId, OnboardingManuscriptBuildRequest request)
|
||||
{
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
using var result = await connection.QueryMultipleAsync(
|
||||
"dbo.Onboarding_ManuscriptBuild_Create",
|
||||
new
|
||||
{
|
||||
UserID = userId,
|
||||
request.PreviewID,
|
||||
request.ProjectID,
|
||||
request.BookID,
|
||||
request.DocumentGuid,
|
||||
BindingVersion = 1,
|
||||
ChaptersJson = JsonSerializer.Serialize(request.Chapters),
|
||||
ScenesJson = JsonSerializer.Serialize(request.Scenes),
|
||||
CharactersJson = JsonSerializer.Serialize(request.Characters)
|
||||
},
|
||||
commandType: CommandType.StoredProcedure);
|
||||
|
||||
var header = await result.ReadSingleOrDefaultAsync<OnboardingManuscriptBuildResult>();
|
||||
if (header is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var chapters = (await result.ReadAsync<OnboardingBuildChapterMap>()).ToList();
|
||||
var scenes = (await result.ReadAsync<OnboardingBuildSceneMap>()).ToList();
|
||||
var characters = (await result.ReadAsync<OnboardingBuildCharacterMap>()).ToList();
|
||||
return new OnboardingManuscriptBuildResult
|
||||
{
|
||||
BuildID = header.BuildID,
|
||||
PreviewID = header.PreviewID,
|
||||
ProjectID = header.ProjectID,
|
||||
BookID = header.BookID,
|
||||
ManuscriptDocumentID = header.ManuscriptDocumentID,
|
||||
Status = header.Status,
|
||||
MarkerStatus = header.MarkerStatus,
|
||||
ChaptersCreated = header.ChaptersCreated,
|
||||
ScenesCreated = header.ScenesCreated,
|
||||
CharactersCreated = header.CharactersCreated,
|
||||
CharactersReused = header.CharactersReused,
|
||||
SceneAppearancesCreated = header.SceneAppearancesCreated,
|
||||
AlreadyBuilt = header.AlreadyBuilt,
|
||||
Message = header.Message,
|
||||
ChapterMappings = chapters,
|
||||
SceneMappings = scenes,
|
||||
CharacterMappings = characters
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class OnboardingManuscriptBuildRequest
|
||||
{
|
||||
public Guid PreviewID { get; init; }
|
||||
public int ProjectID { get; init; }
|
||||
public int BookID { get; init; }
|
||||
public Guid DocumentGuid { get; init; }
|
||||
public IReadOnlyList<OnboardingBuildChapterRequest> Chapters { get; init; } = [];
|
||||
public IReadOnlyList<OnboardingBuildSceneRequest> Scenes { get; init; } = [];
|
||||
public IReadOnlyList<OnboardingBuildCharacterRequest> Characters { get; init; } = [];
|
||||
}
|
||||
|
||||
public sealed class OnboardingBuildChapterRequest
|
||||
{
|
||||
public string TemporaryChapterKey { get; init; } = string.Empty;
|
||||
public string Title { get; init; } = string.Empty;
|
||||
public int SortOrder { get; init; }
|
||||
public int WordCount { get; init; }
|
||||
}
|
||||
|
||||
public sealed class OnboardingBuildSceneRequest
|
||||
{
|
||||
public string TemporarySceneKey { get; init; } = string.Empty;
|
||||
public string TemporaryChapterKey { get; init; } = string.Empty;
|
||||
public string Title { get; init; } = string.Empty;
|
||||
public int SortOrder { get; init; }
|
||||
public int WordCount { get; init; }
|
||||
}
|
||||
|
||||
public sealed class OnboardingBuildCharacterRequest
|
||||
{
|
||||
public string TemporaryCharacterKey { get; init; } = string.Empty;
|
||||
public string Name { get; init; } = string.Empty;
|
||||
public int? ExistingCharacterID { get; init; }
|
||||
}
|
||||
@ -11,7 +11,8 @@ namespace PlotLine.Hubs;
|
||||
public sealed class WordCompanionFollowHub(
|
||||
IWordCompanionPresenceService presence,
|
||||
IOnboardingRepository onboarding,
|
||||
IManuscriptScanPreviewStore scanStore) : Hub
|
||||
IManuscriptScanPreviewStore scanStore,
|
||||
IOnboardingService onboardingService) : Hub
|
||||
{
|
||||
public override async Task OnConnectedAsync()
|
||||
{
|
||||
@ -126,6 +127,35 @@ public sealed class WordCompanionFollowHub(
|
||||
return scanState;
|
||||
}
|
||||
|
||||
public async Task<OnboardingManuscriptBuildResult> StartOnboardingProjectBuild(Guid previewId)
|
||||
{
|
||||
var userId = RequireUserId();
|
||||
await Clients.Group(PresenceGroup(userId)).SendAsync("OnboardingBuildProgress", new OnboardingBuildProgress
|
||||
{
|
||||
Status = "Running",
|
||||
Message = "Creating chapters...",
|
||||
PercentComplete = 0,
|
||||
PreviewID = previewId
|
||||
});
|
||||
|
||||
var result = await onboardingService.BuildApprovedStructureAsync(
|
||||
previewId,
|
||||
progress => Clients.Group(PresenceGroup(userId)).SendAsync("OnboardingBuildProgress", progress));
|
||||
if (result is null)
|
||||
{
|
||||
throw new HubException("The reviewed scan could not be found.");
|
||||
}
|
||||
|
||||
var companionConnectionId = await presence.GetCompanionConnectionIdAsync(userId);
|
||||
if (!string.IsNullOrWhiteSpace(companionConnectionId))
|
||||
{
|
||||
await Clients.Client(companionConnectionId).SendAsync("UpdateOnboardingBuildMarkers", result);
|
||||
}
|
||||
|
||||
await Clients.Group(PresenceGroup(userId)).SendAsync("OnboardingBuildCompleted", result);
|
||||
return result;
|
||||
}
|
||||
|
||||
public override async Task OnDisconnectedAsync(Exception? exception)
|
||||
{
|
||||
var status = await presence.DisconnectAsync(Context.ConnectionId);
|
||||
|
||||
@ -153,3 +153,56 @@ public sealed class ManuscriptScanCharacterReviewDecision
|
||||
public string Category { get; init; } = "PossibleCharacter";
|
||||
public int? ExistingCharacterID { get; init; }
|
||||
}
|
||||
|
||||
public sealed class OnboardingManuscriptBuildResult
|
||||
{
|
||||
public int BuildID { get; init; }
|
||||
public Guid PreviewID { get; init; }
|
||||
public int ProjectID { get; init; }
|
||||
public int BookID { get; init; }
|
||||
public int? ManuscriptDocumentID { get; init; }
|
||||
public string Status { get; init; } = "Built";
|
||||
public string MarkerStatus { get; init; } = "Pending";
|
||||
public int ChaptersCreated { get; init; }
|
||||
public int ScenesCreated { get; init; }
|
||||
public int CharactersCreated { get; init; }
|
||||
public int CharactersReused { get; init; }
|
||||
public int SceneAppearancesCreated { get; init; }
|
||||
public bool AlreadyBuilt { get; init; }
|
||||
public string Message { get; init; } = string.Empty;
|
||||
public IReadOnlyList<OnboardingBuildChapterMap> ChapterMappings { get; init; } = [];
|
||||
public IReadOnlyList<OnboardingBuildSceneMap> SceneMappings { get; init; } = [];
|
||||
public IReadOnlyList<OnboardingBuildCharacterMap> CharacterMappings { get; init; } = [];
|
||||
public string? MarkerWarning { get; init; }
|
||||
}
|
||||
|
||||
public sealed class OnboardingBuildChapterMap
|
||||
{
|
||||
public string TemporaryChapterKey { get; init; } = string.Empty;
|
||||
public int ChapterID { get; init; }
|
||||
}
|
||||
|
||||
public sealed class OnboardingBuildSceneMap
|
||||
{
|
||||
public string TemporarySceneKey { get; init; } = string.Empty;
|
||||
public int SceneID { get; init; }
|
||||
}
|
||||
|
||||
public sealed class OnboardingBuildCharacterMap
|
||||
{
|
||||
public string TemporaryCharacterKey { get; init; } = string.Empty;
|
||||
public int CharacterID { get; init; }
|
||||
public bool Created { get; init; }
|
||||
}
|
||||
|
||||
public sealed class OnboardingBuildProgress
|
||||
{
|
||||
public string Status { get; init; } = "Running";
|
||||
public string Message { get; init; } = string.Empty;
|
||||
public int? PercentComplete { get; init; }
|
||||
public int ChaptersCreated { get; init; }
|
||||
public int ScenesCreated { get; init; }
|
||||
public int CharactersCreated { get; init; }
|
||||
public int SceneAppearancesCreated { get; init; }
|
||||
public Guid? PreviewID { get; init; }
|
||||
}
|
||||
|
||||
@ -132,6 +132,7 @@ public class Program
|
||||
builder.Services.AddScoped<IImportRepository, ImportRepository>();
|
||||
builder.Services.AddScoped<IWordCompanionRepository, WordCompanionRepository>();
|
||||
builder.Services.AddScoped<IOnboardingRepository, OnboardingRepository>();
|
||||
builder.Services.AddScoped<IOnboardingBuildRepository, OnboardingBuildRepository>();
|
||||
builder.Services.AddScoped<IProjectAccessRepository, ProjectAccessRepository>();
|
||||
builder.Services.AddScoped<IProjectCollaborationRepository, ProjectCollaborationRepository>();
|
||||
builder.Services.AddScoped<IProjectActivityRepository, ProjectActivityRepository>();
|
||||
|
||||
@ -15,6 +15,9 @@ public interface IManuscriptScanPreviewStore
|
||||
Task<ManuscriptScanPreview?> GetPreviewAsync(int userId, Guid previewId);
|
||||
Task<ManuscriptScanReviewDecision?> GetReviewAsync(int userId, Guid previewId);
|
||||
Task<ManuscriptScanReviewDecision?> SaveReviewAsync(int userId, ManuscriptScanReviewDecision review);
|
||||
Task<OnboardingBuildProgress> BuildProgressAsync(int userId, Guid previewId, string message, int percentComplete, OnboardingManuscriptBuildResult? result = null);
|
||||
Task<OnboardingManuscriptBuildResult?> GetBuildResultAsync(int userId, Guid previewId);
|
||||
Task<OnboardingManuscriptBuildResult?> SaveBuildResultAsync(int userId, OnboardingManuscriptBuildResult result);
|
||||
}
|
||||
|
||||
public sealed class ManuscriptScanPreviewStore : IManuscriptScanPreviewStore
|
||||
@ -22,6 +25,8 @@ public sealed class ManuscriptScanPreviewStore : IManuscriptScanPreviewStore
|
||||
private readonly ConcurrentDictionary<(int UserId, int OnboardingId), ScanSession> sessions = new();
|
||||
private readonly ConcurrentDictionary<Guid, ManuscriptScanPreview> previews = new();
|
||||
private readonly ConcurrentDictionary<Guid, ManuscriptScanReviewDecision> reviews = new();
|
||||
private readonly ConcurrentDictionary<Guid, OnboardingManuscriptBuildResult> buildResults = new();
|
||||
private readonly ConcurrentDictionary<Guid, OnboardingBuildProgress> buildProgress = new();
|
||||
|
||||
public Task<ManuscriptScanState> StartAsync(int userId, int onboardingId, int projectId, int bookId)
|
||||
{
|
||||
@ -173,6 +178,46 @@ public sealed class ManuscriptScanPreviewStore : IManuscriptScanPreviewStore
|
||||
return Task.FromResult<ManuscriptScanReviewDecision?>(stored);
|
||||
}
|
||||
|
||||
public Task<OnboardingBuildProgress> BuildProgressAsync(int userId, Guid previewId, string message, int percentComplete, OnboardingManuscriptBuildResult? result = null)
|
||||
{
|
||||
if (!previews.TryGetValue(previewId, out var preview) || preview.UserID != userId)
|
||||
{
|
||||
return Task.FromResult(new OnboardingBuildProgress { Status = "Failed", Message = "The scan preview could not be found." });
|
||||
}
|
||||
|
||||
var progress = new OnboardingBuildProgress
|
||||
{
|
||||
Status = result is null ? "Running" : "Complete",
|
||||
Message = message,
|
||||
PercentComplete = percentComplete,
|
||||
ChaptersCreated = result?.ChaptersCreated ?? 0,
|
||||
ScenesCreated = result?.ScenesCreated ?? 0,
|
||||
CharactersCreated = (result?.CharactersCreated ?? 0) + (result?.CharactersReused ?? 0),
|
||||
SceneAppearancesCreated = result?.SceneAppearancesCreated ?? 0,
|
||||
PreviewID = previewId
|
||||
};
|
||||
buildProgress[previewId] = progress;
|
||||
return Task.FromResult(progress);
|
||||
}
|
||||
|
||||
public Task<OnboardingManuscriptBuildResult?> GetBuildResultAsync(int userId, Guid previewId)
|
||||
=> Task.FromResult(previews.TryGetValue(previewId, out var preview)
|
||||
&& preview.UserID == userId
|
||||
&& buildResults.TryGetValue(previewId, out var result)
|
||||
? result
|
||||
: null);
|
||||
|
||||
public Task<OnboardingManuscriptBuildResult?> SaveBuildResultAsync(int userId, OnboardingManuscriptBuildResult result)
|
||||
{
|
||||
if (!previews.TryGetValue(result.PreviewID, out var preview) || preview.UserID != userId)
|
||||
{
|
||||
return Task.FromResult<OnboardingManuscriptBuildResult?>(null);
|
||||
}
|
||||
|
||||
buildResults[result.PreviewID] = result;
|
||||
return Task.FromResult<OnboardingManuscriptBuildResult?>(result);
|
||||
}
|
||||
|
||||
private static ManuscriptScanState ToState(ScanSession session)
|
||||
=> new()
|
||||
{
|
||||
|
||||
@ -10,6 +10,8 @@ public interface IOnboardingService
|
||||
Task<OnboardingWizardViewModel> GetWizardAsync();
|
||||
Task<ManuscriptScanReviewViewModel?> GetScanReviewAsync(Guid? previewId = null);
|
||||
Task<ManuscriptScanReviewViewModel?> SaveScanReviewAsync(ManuscriptScanReviewForm form, bool readyToImport);
|
||||
Task<OnboardingManuscriptBuildResult?> BuildApprovedStructureAsync(Guid previewId, Func<OnboardingBuildProgress, Task>? progress = null);
|
||||
Task<OnboardingManuscriptBuildResult?> GetBuildResultAsync(Guid previewId);
|
||||
Task<bool> ShouldShowOnboardingAsync();
|
||||
Task<OnboardingNudgeViewModel> GetDashboardNudgeAsync();
|
||||
Task<bool> ShouldShowWordCompanionHeaderAsync();
|
||||
@ -31,6 +33,7 @@ public sealed class OnboardingService(
|
||||
IWordCompanionPresenceService companionPresence,
|
||||
IManuscriptScanPreviewStore scanPreviews,
|
||||
ICharacterRepository characters,
|
||||
IOnboardingBuildRepository builds,
|
||||
ICurrentUserService currentUser) : IOnboardingService
|
||||
{
|
||||
private static readonly HashSet<string> WritingJourneys = new(StringComparer.Ordinal)
|
||||
@ -184,6 +187,85 @@ public sealed class OnboardingService(
|
||||
return saved is null ? null : BuildScanReviewViewModel((userId, preview, saved, project, book, existingCharacters));
|
||||
}
|
||||
|
||||
public async Task<OnboardingManuscriptBuildResult?> BuildApprovedStructureAsync(Guid previewId, Func<OnboardingBuildProgress, Task>? progress = null)
|
||||
{
|
||||
var context = await GetScanReviewContextAsync(previewId);
|
||||
if (context is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var (userId, preview, review, _, _, _) = context.Value;
|
||||
if (!string.Equals(review.Status, ManuscriptScanReviewStatuses.ReadyToImport, StringComparison.Ordinal))
|
||||
{
|
||||
throw new InvalidOperationException("Review the scan and choose Save and continue before building the project structure.");
|
||||
}
|
||||
|
||||
await PublishBuildProgress(userId, preview.PreviewID, "Creating chapters...", 10, progress);
|
||||
ValidateScanReview(review, preview);
|
||||
|
||||
var chapterDecisions = review.Chapters.Where(chapter => chapter.Include).OrderBy(chapter => chapter.ChapterNumber).ToList();
|
||||
var chapterKeys = chapterDecisions.Select(chapter => chapter.TemporaryChapterKey).ToHashSet(StringComparer.Ordinal);
|
||||
var sceneDecisions = review.Scenes
|
||||
.Where(scene => scene.Include && chapterKeys.Contains(scene.TemporaryChapterKey))
|
||||
.OrderBy(scene => chapterDecisions.FindIndex(chapter => chapter.TemporaryChapterKey == scene.TemporaryChapterKey))
|
||||
.ThenBy(scene => scene.SceneNumberWithinChapter)
|
||||
.ToList();
|
||||
var characterDecisions = review.Characters
|
||||
.Where(character => character.Include && !string.Equals(character.Category, "Excluded", StringComparison.OrdinalIgnoreCase))
|
||||
.ToList();
|
||||
|
||||
await PublishBuildProgress(userId, preview.PreviewID, "Creating scenes...", 35, progress);
|
||||
var request = new OnboardingManuscriptBuildRequest
|
||||
{
|
||||
PreviewID = preview.PreviewID,
|
||||
ProjectID = preview.ProjectID,
|
||||
BookID = preview.BookID,
|
||||
DocumentGuid = Guid.TryParse(preview.CompanionDocumentIdentifier, out var documentGuid) ? documentGuid : Guid.NewGuid(),
|
||||
Chapters = chapterDecisions.Select((chapter, index) => new OnboardingBuildChapterRequest
|
||||
{
|
||||
TemporaryChapterKey = chapter.TemporaryChapterKey,
|
||||
Title = chapter.Title,
|
||||
SortOrder = (index + 1) * 10,
|
||||
WordCount = preview.Chapters.FirstOrDefault(item => item.TemporaryChapterKey == chapter.TemporaryChapterKey)?.WordCount ?? 0
|
||||
}).ToList(),
|
||||
Scenes = sceneDecisions.Select((scene, index) => new OnboardingBuildSceneRequest
|
||||
{
|
||||
TemporarySceneKey = scene.TemporarySceneKey,
|
||||
TemporaryChapterKey = scene.TemporaryChapterKey,
|
||||
Title = string.IsNullOrWhiteSpace(scene.Title) ? $"Scene {scene.SceneNumberWithinChapter}" : scene.Title!,
|
||||
SortOrder = (index + 1) * 10,
|
||||
WordCount = preview.Scenes.FirstOrDefault(item => item.TemporarySceneKey == scene.TemporarySceneKey)?.WordCount ?? 0
|
||||
}).ToList(),
|
||||
Characters = characterDecisions.Select(character => new OnboardingBuildCharacterRequest
|
||||
{
|
||||
TemporaryCharacterKey = character.TemporaryCharacterKey,
|
||||
Name = character.Name,
|
||||
ExistingCharacterID = character.ExistingCharacterID
|
||||
}).ToList()
|
||||
};
|
||||
|
||||
await PublishBuildProgress(userId, preview.PreviewID, "Creating characters...", 60, progress);
|
||||
var result = await builds.BuildAsync(userId, request)
|
||||
?? throw new InvalidOperationException("The project structure could not be built. Check the review choices and try again.");
|
||||
await scanPreviews.SaveBuildResultAsync(userId, result);
|
||||
await PublishBuildProgress(userId, preview.PreviewID, "Finalising project...", 90, progress);
|
||||
await PublishBuildProgress(userId, preview.PreviewID, result.Message, 100, progress, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<OnboardingManuscriptBuildResult?> GetBuildResultAsync(Guid previewId)
|
||||
=> await scanPreviews.GetBuildResultAsync(RequireUserId(), previewId);
|
||||
|
||||
private async Task PublishBuildProgress(int userId, Guid previewId, string message, int percent, Func<OnboardingBuildProgress, Task>? publish, OnboardingManuscriptBuildResult? result = null)
|
||||
{
|
||||
var state = await scanPreviews.BuildProgressAsync(userId, previewId, message, percent, result);
|
||||
if (publish is not null)
|
||||
{
|
||||
await publish(state);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<(int UserId, ManuscriptScanPreview Preview, ManuscriptScanReviewDecision Review, Project Project, Book Book, IReadOnlyList<Character> ExistingCharacters)?> GetScanReviewContextAsync(Guid? previewId)
|
||||
{
|
||||
var userId = RequireUserId();
|
||||
|
||||
296
PlotLine/Sql/113_Phase20F_OnboardingManuscriptBuild.sql
Normal file
296
PlotLine/Sql/113_Phase20F_OnboardingManuscriptBuild.sql
Normal file
@ -0,0 +1,296 @@
|
||||
SET ANSI_NULLS ON;
|
||||
GO
|
||||
SET QUOTED_IDENTIFIER ON;
|
||||
GO
|
||||
|
||||
IF OBJECT_ID(N'dbo.OnboardingManuscriptBuilds', N'U') IS NULL
|
||||
BEGIN
|
||||
CREATE TABLE dbo.OnboardingManuscriptBuilds
|
||||
(
|
||||
OnboardingManuscriptBuildID int IDENTITY(1,1) NOT NULL CONSTRAINT PK_OnboardingManuscriptBuilds PRIMARY KEY,
|
||||
PreviewID uniqueidentifier NOT NULL,
|
||||
UserID int NOT NULL,
|
||||
ProjectID int NOT NULL,
|
||||
BookID int NOT NULL,
|
||||
ManuscriptDocumentID int NULL,
|
||||
Status nvarchar(40) NOT NULL CONSTRAINT DF_OnboardingManuscriptBuilds_Status DEFAULT N'Built',
|
||||
MarkerStatus nvarchar(40) NOT NULL CONSTRAINT DF_OnboardingManuscriptBuilds_MarkerStatus DEFAULT N'Pending',
|
||||
ChaptersCreated int NOT NULL CONSTRAINT DF_OnboardingManuscriptBuilds_ChaptersCreated DEFAULT 0,
|
||||
ScenesCreated int NOT NULL CONSTRAINT DF_OnboardingManuscriptBuilds_ScenesCreated DEFAULT 0,
|
||||
CharactersCreated int NOT NULL CONSTRAINT DF_OnboardingManuscriptBuilds_CharactersCreated DEFAULT 0,
|
||||
CharactersReused int NOT NULL CONSTRAINT DF_OnboardingManuscriptBuilds_CharactersReused DEFAULT 0,
|
||||
SceneAppearancesCreated int NOT NULL CONSTRAINT DF_OnboardingManuscriptBuilds_SceneAppearancesCreated DEFAULT 0,
|
||||
CreatedUtc datetime2 NOT NULL CONSTRAINT DF_OnboardingManuscriptBuilds_CreatedUtc DEFAULT SYSUTCDATETIME(),
|
||||
UpdatedUtc datetime2 NOT NULL CONSTRAINT DF_OnboardingManuscriptBuilds_UpdatedUtc DEFAULT SYSUTCDATETIME(),
|
||||
CONSTRAINT UQ_OnboardingManuscriptBuilds_PreviewID UNIQUE (PreviewID),
|
||||
CONSTRAINT FK_OnboardingManuscriptBuilds_AppUser FOREIGN KEY (UserID) REFERENCES dbo.AppUser(UserID),
|
||||
CONSTRAINT FK_OnboardingManuscriptBuilds_Projects FOREIGN KEY (ProjectID) REFERENCES dbo.Projects(ProjectID),
|
||||
CONSTRAINT FK_OnboardingManuscriptBuilds_Books FOREIGN KEY (BookID) REFERENCES dbo.Books(BookID),
|
||||
CONSTRAINT FK_OnboardingManuscriptBuilds_ManuscriptDocuments FOREIGN KEY (ManuscriptDocumentID) REFERENCES dbo.ManuscriptDocuments(ManuscriptDocumentID)
|
||||
);
|
||||
END;
|
||||
GO
|
||||
|
||||
IF OBJECT_ID(N'dbo.OnboardingManuscriptBuildChapterMap', N'U') IS NULL
|
||||
BEGIN
|
||||
CREATE TABLE dbo.OnboardingManuscriptBuildChapterMap
|
||||
(
|
||||
OnboardingManuscriptBuildID int NOT NULL,
|
||||
TemporaryChapterKey nvarchar(80) NOT NULL,
|
||||
ChapterID int NOT NULL,
|
||||
CONSTRAINT PK_OnboardingManuscriptBuildChapterMap PRIMARY KEY (OnboardingManuscriptBuildID, TemporaryChapterKey),
|
||||
CONSTRAINT FK_OnboardingManuscriptBuildChapterMap_Build FOREIGN KEY (OnboardingManuscriptBuildID) REFERENCES dbo.OnboardingManuscriptBuilds(OnboardingManuscriptBuildID),
|
||||
CONSTRAINT FK_OnboardingManuscriptBuildChapterMap_Chapters FOREIGN KEY (ChapterID) REFERENCES dbo.Chapters(ChapterID)
|
||||
);
|
||||
END;
|
||||
GO
|
||||
|
||||
IF OBJECT_ID(N'dbo.OnboardingManuscriptBuildSceneMap', N'U') IS NULL
|
||||
BEGIN
|
||||
CREATE TABLE dbo.OnboardingManuscriptBuildSceneMap
|
||||
(
|
||||
OnboardingManuscriptBuildID int NOT NULL,
|
||||
TemporarySceneKey nvarchar(80) NOT NULL,
|
||||
SceneID int NOT NULL,
|
||||
CONSTRAINT PK_OnboardingManuscriptBuildSceneMap PRIMARY KEY (OnboardingManuscriptBuildID, TemporarySceneKey),
|
||||
CONSTRAINT FK_OnboardingManuscriptBuildSceneMap_Build FOREIGN KEY (OnboardingManuscriptBuildID) REFERENCES dbo.OnboardingManuscriptBuilds(OnboardingManuscriptBuildID),
|
||||
CONSTRAINT FK_OnboardingManuscriptBuildSceneMap_Scenes FOREIGN KEY (SceneID) REFERENCES dbo.Scenes(SceneID)
|
||||
);
|
||||
END;
|
||||
GO
|
||||
|
||||
IF OBJECT_ID(N'dbo.OnboardingManuscriptBuildCharacterMap', N'U') IS NULL
|
||||
BEGIN
|
||||
CREATE TABLE dbo.OnboardingManuscriptBuildCharacterMap
|
||||
(
|
||||
OnboardingManuscriptBuildID int NOT NULL,
|
||||
TemporaryCharacterKey nvarchar(80) NOT NULL,
|
||||
CharacterID int NOT NULL,
|
||||
Created bit NOT NULL,
|
||||
CONSTRAINT PK_OnboardingManuscriptBuildCharacterMap PRIMARY KEY (OnboardingManuscriptBuildID, TemporaryCharacterKey),
|
||||
CONSTRAINT FK_OnboardingManuscriptBuildCharacterMap_Build FOREIGN KEY (OnboardingManuscriptBuildID) REFERENCES dbo.OnboardingManuscriptBuilds(OnboardingManuscriptBuildID),
|
||||
CONSTRAINT FK_OnboardingManuscriptBuildCharacterMap_Characters FOREIGN KEY (CharacterID) REFERENCES dbo.Characters(CharacterID)
|
||||
);
|
||||
END;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.Onboarding_ManuscriptBuild_Create
|
||||
@UserID int,
|
||||
@PreviewID uniqueidentifier,
|
||||
@ProjectID int,
|
||||
@BookID int,
|
||||
@DocumentGuid uniqueidentifier,
|
||||
@BindingVersion int = 1,
|
||||
@ChaptersJson nvarchar(max),
|
||||
@ScenesJson nvarchar(max),
|
||||
@CharactersJson 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 @ExistingBuildID int;
|
||||
SELECT @ExistingBuildID = OnboardingManuscriptBuildID
|
||||
FROM dbo.OnboardingManuscriptBuilds
|
||||
WHERE PreviewID = @PreviewID AND UserID = @UserID;
|
||||
|
||||
IF @ExistingBuildID IS NOT NULL
|
||||
BEGIN
|
||||
SELECT OnboardingManuscriptBuildID AS BuildID, PreviewID, ProjectID, BookID, ManuscriptDocumentID, Status, MarkerStatus,
|
||||
ChaptersCreated, ScenesCreated, CharactersCreated, CharactersReused, SceneAppearancesCreated,
|
||||
CAST(1 AS bit) AS AlreadyBuilt,
|
||||
N'This manuscript has already been built into PlotDirector.' AS Message
|
||||
FROM dbo.OnboardingManuscriptBuilds
|
||||
WHERE OnboardingManuscriptBuildID = @ExistingBuildID;
|
||||
|
||||
SELECT TemporaryChapterKey, ChapterID FROM dbo.OnboardingManuscriptBuildChapterMap WHERE OnboardingManuscriptBuildID = @ExistingBuildID ORDER BY TemporaryChapterKey;
|
||||
SELECT TemporarySceneKey, SceneID FROM dbo.OnboardingManuscriptBuildSceneMap WHERE OnboardingManuscriptBuildID = @ExistingBuildID ORDER BY TemporarySceneKey;
|
||||
SELECT TemporaryCharacterKey, CharacterID, Created FROM dbo.OnboardingManuscriptBuildCharacterMap WHERE OnboardingManuscriptBuildID = @ExistingBuildID ORDER BY TemporaryCharacterKey;
|
||||
RETURN;
|
||||
END;
|
||||
|
||||
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.ProjectID = @ProjectID
|
||||
AND b.IsArchived = 0
|
||||
AND p.IsArchived = 0
|
||||
AND pua.UserID = @UserID
|
||||
AND pua.IsActive = 1
|
||||
)
|
||||
RETURN;
|
||||
|
||||
DECLARE @Chapters table (TemporaryChapterKey nvarchar(80) NOT NULL PRIMARY KEY, Title nvarchar(200) NOT NULL, SortOrder int NOT NULL, WordCount int NOT NULL);
|
||||
DECLARE @Scenes table (TemporarySceneKey nvarchar(80) NOT NULL PRIMARY KEY, TemporaryChapterKey nvarchar(80) NOT NULL, Title nvarchar(200) NOT NULL, SortOrder int NOT NULL, WordCount int NOT NULL);
|
||||
DECLARE @Characters table (TemporaryCharacterKey nvarchar(80) NOT NULL PRIMARY KEY, Name nvarchar(200) NOT NULL, ExistingCharacterID int NULL);
|
||||
|
||||
INSERT @Chapters (TemporaryChapterKey, Title, SortOrder, WordCount)
|
||||
SELECT LEFT(JSON_VALUE(value, '$.TemporaryChapterKey'), 80),
|
||||
LEFT(LTRIM(RTRIM(JSON_VALUE(value, '$.Title'))), 200),
|
||||
TRY_CONVERT(int, JSON_VALUE(value, '$.SortOrder')),
|
||||
COALESCE(TRY_CONVERT(int, JSON_VALUE(value, '$.WordCount')), 0)
|
||||
FROM OPENJSON(@ChaptersJson)
|
||||
WHERE NULLIF(JSON_VALUE(value, '$.TemporaryChapterKey'), N'') IS NOT NULL
|
||||
AND NULLIF(LTRIM(RTRIM(JSON_VALUE(value, '$.Title'))), N'') IS NOT NULL;
|
||||
|
||||
INSERT @Scenes (TemporarySceneKey, TemporaryChapterKey, Title, SortOrder, WordCount)
|
||||
SELECT LEFT(JSON_VALUE(value, '$.TemporarySceneKey'), 80),
|
||||
LEFT(JSON_VALUE(value, '$.TemporaryChapterKey'), 80),
|
||||
LEFT(LTRIM(RTRIM(JSON_VALUE(value, '$.Title'))), 200),
|
||||
TRY_CONVERT(int, JSON_VALUE(value, '$.SortOrder')),
|
||||
COALESCE(TRY_CONVERT(int, JSON_VALUE(value, '$.WordCount')), 0)
|
||||
FROM OPENJSON(@ScenesJson)
|
||||
WHERE NULLIF(JSON_VALUE(value, '$.TemporarySceneKey'), N'') IS NOT NULL
|
||||
AND NULLIF(JSON_VALUE(value, '$.TemporaryChapterKey'), N'') IS NOT NULL
|
||||
AND NULLIF(LTRIM(RTRIM(JSON_VALUE(value, '$.Title'))), N'') IS NOT NULL;
|
||||
|
||||
INSERT @Characters (TemporaryCharacterKey, Name, ExistingCharacterID)
|
||||
SELECT LEFT(JSON_VALUE(value, '$.TemporaryCharacterKey'), 80),
|
||||
LEFT(LTRIM(RTRIM(JSON_VALUE(value, '$.Name'))), 200),
|
||||
TRY_CONVERT(int, JSON_VALUE(value, '$.ExistingCharacterID'))
|
||||
FROM OPENJSON(@CharactersJson)
|
||||
WHERE NULLIF(JSON_VALUE(value, '$.TemporaryCharacterKey'), N'') IS NOT NULL
|
||||
AND NULLIF(LTRIM(RTRIM(JSON_VALUE(value, '$.Name'))), N'') IS NOT NULL;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM @Chapters) OR EXISTS (SELECT 1 FROM @Scenes s WHERE NOT EXISTS (SELECT 1 FROM @Chapters c WHERE c.TemporaryChapterKey = s.TemporaryChapterKey))
|
||||
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;
|
||||
|
||||
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);
|
||||
|
||||
DECLARE @ChapterMap table (TemporaryChapterKey nvarchar(80) NOT NULL PRIMARY KEY, ChapterID int NOT NULL);
|
||||
DECLARE @SceneMap table (TemporarySceneKey nvarchar(80) NOT NULL PRIMARY KEY, SceneID int NOT NULL);
|
||||
DECLARE @CharacterMap table (TemporaryCharacterKey nvarchar(80) NOT NULL PRIMARY KEY, CharacterID int NOT NULL, Created bit NOT NULL);
|
||||
DECLARE @BuildID int;
|
||||
DECLARE @ManuscriptDocumentID int;
|
||||
DECLARE @LinkedUtc datetime2 = SYSUTCDATETIME();
|
||||
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
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;
|
||||
|
||||
INSERT dbo.OnboardingManuscriptBuilds (PreviewID, UserID, ProjectID, BookID, ManuscriptDocumentID)
|
||||
VALUES (@PreviewID, @UserID, @ProjectID, @BookID, @ManuscriptDocumentID);
|
||||
SET @BuildID = CAST(SCOPE_IDENTITY() AS int);
|
||||
|
||||
DECLARE @TempChapterKey nvarchar(80), @Title nvarchar(200), @SortOrder int, @WordCount int, @ChapterID int, @ChapterNumber decimal(9,2);
|
||||
DECLARE chapter_cursor CURSOR LOCAL FAST_FORWARD FOR SELECT TemporaryChapterKey, Title, SortOrder, WordCount FROM @Chapters ORDER BY SortOrder, TemporaryChapterKey;
|
||||
OPEN chapter_cursor;
|
||||
FETCH NEXT FROM chapter_cursor INTO @TempChapterKey, @Title, @SortOrder, @WordCount;
|
||||
WHILE @@FETCH_STATUS = 0
|
||||
BEGIN
|
||||
SET @ChapterNumber = (SELECT COUNT(*) + 1 FROM @ChapterMap);
|
||||
INSERT dbo.Chapters (BookID, ChapterNumber, ChapterTitle, Summary, SortOrder, RevisionStatusID, IsLinkedToManuscript, ManuscriptDocumentID, ExternalReference, LinkedUtc)
|
||||
VALUES (@BookID, @ChapterNumber, @Title, NULL, @SortOrder, @PlannedRevisionStatusID, 1, @ManuscriptDocumentID, @TempChapterKey, @LinkedUtc);
|
||||
SET @ChapterID = CAST(SCOPE_IDENTITY() AS int);
|
||||
INSERT @ChapterMap VALUES (@TempChapterKey, @ChapterID);
|
||||
INSERT dbo.OnboardingManuscriptBuildChapterMap VALUES (@BuildID, @TempChapterKey, @ChapterID);
|
||||
FETCH NEXT FROM chapter_cursor INTO @TempChapterKey, @Title, @SortOrder, @WordCount;
|
||||
END;
|
||||
CLOSE chapter_cursor; DEALLOCATE chapter_cursor;
|
||||
|
||||
DECLARE @TempSceneKey nvarchar(80), @TempSceneChapterKey nvarchar(80), @SceneID int, @SceneNumber decimal(9,2);
|
||||
DECLARE scene_cursor CURSOR LOCAL FAST_FORWARD FOR
|
||||
SELECT TemporarySceneKey, TemporaryChapterKey, Title, SortOrder, WordCount FROM @Scenes ORDER BY TemporaryChapterKey, SortOrder, TemporarySceneKey;
|
||||
OPEN scene_cursor;
|
||||
FETCH NEXT FROM scene_cursor INTO @TempSceneKey, @TempSceneChapterKey, @Title, @SortOrder, @WordCount;
|
||||
WHILE @@FETCH_STATUS = 0
|
||||
BEGIN
|
||||
SELECT @ChapterID = ChapterID FROM @ChapterMap WHERE TemporaryChapterKey = @TempSceneChapterKey;
|
||||
SET @SceneNumber = (SELECT COUNT(*) + 1 FROM @SceneMap sm INNER JOIN @Scenes s ON s.TemporarySceneKey = sm.TemporarySceneKey WHERE s.TemporaryChapterKey = @TempSceneChapterKey);
|
||||
IF @SceneNumber = 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 = @SceneNumber, SceneTitle = @Title, SortOrder = @SortOrder, IsLinkedToManuscript = 1,
|
||||
ManuscriptDocumentID = @ManuscriptDocumentID, ExternalReference = @TempSceneKey, 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, @SceneNumber, @Title, NULL, @SortOrder, @DefaultTimeModeID, @DefaultTimeConfidenceID, @PlannedRevisionStatusID, 1, @ManuscriptDocumentID, @TempSceneKey, @LinkedUtc);
|
||||
SET @SceneID = CAST(SCOPE_IDENTITY() AS int);
|
||||
END;
|
||||
IF EXISTS (SELECT 1 FROM dbo.SceneWorkflow WHERE SceneID = @SceneID)
|
||||
UPDATE dbo.SceneWorkflow SET ActualWordCount = @WordCount, UpdatedDate = SYSUTCDATETIME() WHERE SceneID = @SceneID;
|
||||
ELSE
|
||||
INSERT dbo.SceneWorkflow (SceneID, ActualWordCount) VALUES (@SceneID, @WordCount);
|
||||
INSERT @SceneMap VALUES (@TempSceneKey, @SceneID);
|
||||
INSERT dbo.OnboardingManuscriptBuildSceneMap VALUES (@BuildID, @TempSceneKey, @SceneID);
|
||||
FETCH NEXT FROM scene_cursor INTO @TempSceneKey, @TempSceneChapterKey, @Title, @SortOrder, @WordCount;
|
||||
END;
|
||||
CLOSE scene_cursor; DEALLOCATE scene_cursor;
|
||||
|
||||
DECLARE @TempCharacterKey nvarchar(80), @ExistingCharacterID int, @CharacterID int, @Created bit;
|
||||
DECLARE character_cursor CURSOR LOCAL FAST_FORWARD FOR SELECT TemporaryCharacterKey, Name, ExistingCharacterID FROM @Characters ORDER BY Name, TemporaryCharacterKey;
|
||||
OPEN character_cursor;
|
||||
FETCH NEXT FROM character_cursor INTO @TempCharacterKey, @Title, @ExistingCharacterID;
|
||||
WHILE @@FETCH_STATUS = 0
|
||||
BEGIN
|
||||
SET @Created = 0;
|
||||
SELECT @CharacterID = CharacterID FROM dbo.Characters WHERE CharacterID = @ExistingCharacterID AND ProjectID = @ProjectID AND IsArchived = 0;
|
||||
IF @CharacterID IS NULL
|
||||
SELECT TOP (1) @CharacterID = CharacterID FROM dbo.Characters WHERE ProjectID = @ProjectID AND IsArchived = 0 AND UPPER(LTRIM(RTRIM(CharacterName))) = UPPER(@Title);
|
||||
IF @CharacterID IS NULL
|
||||
BEGIN
|
||||
INSERT dbo.Characters (ProjectID, CharacterName, ShortName, CharacterImportance, ShowInQuickAddBar, DefaultDescription)
|
||||
VALUES (@ProjectID, @Title, NULL, 8, 1, NULL);
|
||||
SET @CharacterID = CAST(SCOPE_IDENTITY() AS int);
|
||||
SET @Created = 1;
|
||||
END;
|
||||
INSERT @CharacterMap VALUES (@TempCharacterKey, @CharacterID, @Created);
|
||||
INSERT dbo.OnboardingManuscriptBuildCharacterMap VALUES (@BuildID, @TempCharacterKey, @CharacterID, @Created);
|
||||
SET @CharacterID = NULL;
|
||||
FETCH NEXT FROM character_cursor INTO @TempCharacterKey, @Title, @ExistingCharacterID;
|
||||
END;
|
||||
CLOSE character_cursor; DEALLOCATE character_cursor;
|
||||
|
||||
UPDATE dbo.OnboardingManuscriptBuilds
|
||||
SET ChaptersCreated = (SELECT COUNT(*) FROM @ChapterMap),
|
||||
ScenesCreated = (SELECT COUNT(*) FROM @SceneMap),
|
||||
CharactersCreated = (SELECT COUNT(*) FROM @CharacterMap WHERE Created = 1),
|
||||
CharactersReused = (SELECT COUNT(*) FROM @CharacterMap WHERE Created = 0),
|
||||
UpdatedUtc = SYSUTCDATETIME()
|
||||
WHERE OnboardingManuscriptBuildID = @BuildID;
|
||||
|
||||
COMMIT TRANSACTION;
|
||||
|
||||
SELECT OnboardingManuscriptBuildID AS BuildID, PreviewID, ProjectID, BookID, ManuscriptDocumentID, Status, MarkerStatus,
|
||||
ChaptersCreated, ScenesCreated, CharactersCreated, CharactersReused, SceneAppearancesCreated,
|
||||
CAST(0 AS bit) AS AlreadyBuilt,
|
||||
N'Your project structure has been created.' AS Message
|
||||
FROM dbo.OnboardingManuscriptBuilds WHERE OnboardingManuscriptBuildID = @BuildID;
|
||||
SELECT TemporaryChapterKey, ChapterID FROM dbo.OnboardingManuscriptBuildChapterMap WHERE OnboardingManuscriptBuildID = @BuildID ORDER BY TemporaryChapterKey;
|
||||
SELECT TemporarySceneKey, SceneID FROM dbo.OnboardingManuscriptBuildSceneMap WHERE OnboardingManuscriptBuildID = @BuildID ORDER BY TemporarySceneKey;
|
||||
SELECT TemporaryCharacterKey, CharacterID, Created FROM dbo.OnboardingManuscriptBuildCharacterMap WHERE OnboardingManuscriptBuildID = @BuildID ORDER BY TemporaryCharacterKey;
|
||||
END;
|
||||
GO
|
||||
43
PlotLine/Views/Onboarding/BuildComplete.cshtml
Normal file
43
PlotLine/Views/Onboarding/BuildComplete.cshtml
Normal file
@ -0,0 +1,43 @@
|
||||
@model OnboardingManuscriptBuildResult
|
||||
@{
|
||||
ViewData["Title"] = "Project structure created";
|
||||
}
|
||||
|
||||
<section class="onboarding-shell" aria-labelledby="build-complete-title">
|
||||
<div class="onboarding-panel onboarding-review-panel">
|
||||
<div class="onboarding-copy">
|
||||
<p class="eyebrow">Build Project</p>
|
||||
<h1 id="build-complete-title">Your project structure has been created.</h1>
|
||||
<p>@(Model.AlreadyBuilt ? "This manuscript has already been built into PlotDirector." : "PlotDirector has created the approved chapters, scenes and characters.")</p>
|
||||
@if (!string.IsNullOrWhiteSpace(Model.MarkerWarning))
|
||||
{
|
||||
<p class="text-warning">@Model.MarkerWarning</p>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="onboarding-scan-counts onboarding-review-counts">
|
||||
<div>
|
||||
<span>Chapters</span>
|
||||
<strong>@Model.ChaptersCreated</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Scenes</span>
|
||||
<strong>@Model.ScenesCreated</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Characters</span>
|
||||
<strong>@(Model.CharactersCreated + Model.CharactersReused)</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Scene appearances</span>
|
||||
<strong>@Model.SceneAppearancesCreated</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="onboarding-actions">
|
||||
<a class="btn btn-primary" asp-controller="Projects" asp-action="Details" asp-route-id="@Model.ProjectID">Open Project Overview</a>
|
||||
<a class="btn btn-outline-primary" asp-controller="Writer" asp-action="Index" asp-route-projectId="@Model.ProjectID">Open Writer Workspace</a>
|
||||
<a class="btn btn-outline-secondary" asp-controller="Onboarding" asp-action="Index">Continue to Story Intelligence</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@ -315,7 +315,14 @@
|
||||
</div>
|
||||
@if (string.Equals(Model.ScanState.ReviewStatus, ManuscriptScanReviewStatuses.ReadyToImport, StringComparison.Ordinal))
|
||||
{
|
||||
<p class="onboarding-scan-next">Next: import into PlotDirector.</p>
|
||||
<p class="onboarding-scan-next">Next: build project structure.</p>
|
||||
<button class="btn btn-success"
|
||||
type="button"
|
||||
data-onboarding-build-start
|
||||
data-preview-id="@Model.ScanState.PreviewID">
|
||||
Build Project
|
||||
</button>
|
||||
<p class="onboarding-scan-next" data-onboarding-build-message></p>
|
||||
}
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@ -199,6 +199,7 @@
|
||||
let firstRunAnalysis = null;
|
||||
let firstRunImportStructure = null;
|
||||
let firstRunCharacterCandidatesModel = [];
|
||||
let lastOnboardingScanPreview = null;
|
||||
let firstRunStructureImported = false;
|
||||
let firstRunCharacterStepActive = false;
|
||||
let firstRunCharacterStepActivatedAt = 0;
|
||||
@ -1254,6 +1255,9 @@
|
||||
companionPresenceConnection.on("ScanCurrentDocument", (command) => {
|
||||
void runOnboardingDocumentScan(command);
|
||||
});
|
||||
companionPresenceConnection.on("UpdateOnboardingBuildMarkers", (result) => {
|
||||
void updateOnboardingBuildMarkers(result);
|
||||
});
|
||||
companionPresenceConnection.onclose(() => {
|
||||
if (companionPresenceHeartbeatTimer) {
|
||||
window.clearInterval(companionPresenceHeartbeatTimer);
|
||||
@ -2112,6 +2116,7 @@
|
||||
preview.characterCandidateCount = 0;
|
||||
}
|
||||
await reportOnboardingScanProgress(command, "Preparing preview", 95, preview);
|
||||
lastOnboardingScanPreview = preview;
|
||||
await companionPresenceConnection.invoke("CompleteOnboardingScan", preview);
|
||||
} catch (error) {
|
||||
console.error("Unable to complete onboarding scan.", error);
|
||||
@ -2119,6 +2124,41 @@
|
||||
}
|
||||
};
|
||||
|
||||
const updateOnboardingBuildMarkers = async (result) => {
|
||||
if (!lastOnboardingScanPreview || !wordHostAvailable || !window.Word || typeof window.Word.run !== "function") {
|
||||
throw new Error("Word document access is unavailable.");
|
||||
}
|
||||
|
||||
const chapterMap = new Map((result?.chapterMappings || result?.ChapterMappings || [])
|
||||
.map((item) => [item.temporaryChapterKey || item.TemporaryChapterKey, item.chapterID || item.ChapterID]));
|
||||
const sceneMap = new Map((result?.sceneMappings || result?.SceneMappings || [])
|
||||
.map((item) => [item.temporarySceneKey || item.TemporarySceneKey, item.sceneID || item.SceneID]));
|
||||
|
||||
await window.Word.run(async (context) => {
|
||||
const paragraphs = context.document.body.paragraphs;
|
||||
paragraphs.load("items/text,items/style,items/styleBuiltIn");
|
||||
await context.sync();
|
||||
|
||||
for (const chapter of lastOnboardingScanPreview.chapters || []) {
|
||||
const chapterId = chapterMap.get(chapter.temporaryChapterKey);
|
||||
const index = Number.parseInt(chapter.startPosition, 10);
|
||||
if (chapterId && Number.isInteger(index) && paragraphs.items[index]) {
|
||||
ensureAnchorControl(paragraphs.items[index], "PlotDirector Chapter", `PD-CHAPTER-${chapterId}`, "PD-CHAPTER");
|
||||
}
|
||||
}
|
||||
|
||||
for (const scene of lastOnboardingScanPreview.scenes || []) {
|
||||
const sceneId = sceneMap.get(scene.temporarySceneKey);
|
||||
const index = Number.parseInt(scene.startPosition, 10);
|
||||
if (sceneId && Number.isInteger(index) && paragraphs.items[index]) {
|
||||
ensureAnchorControl(paragraphs.items[index], "PlotDirector Scene", `PD-SCENE-${sceneId}`, "PD-SCENE");
|
||||
}
|
||||
}
|
||||
|
||||
await context.sync();
|
||||
});
|
||||
};
|
||||
|
||||
const friendlyScanError = (error) => {
|
||||
const message = errorText(error);
|
||||
if (/No chapters were detected/i.test(message)) {
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -148,6 +148,19 @@
|
||||
connection.on("OnboardingScanProgress", applyScanState);
|
||||
connection.on("OnboardingScanCompleted", applyScanState);
|
||||
connection.on("OnboardingScanFailed", applyScanState);
|
||||
connection.on("OnboardingBuildProgress", (state) => {
|
||||
const message = field(state, "message", "Message") || "Building project structure...";
|
||||
document.querySelectorAll("[data-onboarding-build-message]").forEach((node) => {
|
||||
const percent = field(state, "percentComplete", "PercentComplete");
|
||||
node.textContent = percent === null || percent === undefined ? message : `${message} ${percent}%`;
|
||||
});
|
||||
});
|
||||
connection.on("OnboardingBuildCompleted", (result) => {
|
||||
const previewId = field(result, "previewID", "PreviewID") || field(result, "previewId", "PreviewId");
|
||||
if (previewId) {
|
||||
window.location.href = `/onboarding/build-complete?previewId=${encodeURIComponent(previewId)}`;
|
||||
}
|
||||
});
|
||||
connection.onreconnecting(() => applyStatus({ status: "Connecting", isConnected: false }));
|
||||
connection.onreconnected(() => connection.invoke("WatchCompanionPresence").then(applyStatus).catch(() => {}));
|
||||
connection.onclose(() => applyStatus({ status: "Offline", isConnected: false }));
|
||||
@ -172,4 +185,27 @@
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll("[data-onboarding-build-start]").forEach((button) => {
|
||||
button.addEventListener("click", async () => {
|
||||
const previewId = button.dataset.previewId;
|
||||
if (!previewId) {
|
||||
return;
|
||||
}
|
||||
button.disabled = true;
|
||||
document.querySelectorAll("[data-onboarding-build-message]").forEach((node) => {
|
||||
node.textContent = "Creating chapters...";
|
||||
});
|
||||
try {
|
||||
const result = await connection.invoke("StartOnboardingProjectBuild", previewId);
|
||||
const returnedPreviewId = field(result, "previewID", "PreviewID") || previewId;
|
||||
window.location.href = `/onboarding/build-complete?previewId=${encodeURIComponent(returnedPreviewId)}`;
|
||||
} catch (error) {
|
||||
button.disabled = false;
|
||||
document.querySelectorAll("[data-onboarding-build-message]").forEach((node) => {
|
||||
node.textContent = error?.message || "The project structure could not be built. Check the review and try again.";
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
})();
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user