Simplify import wizard and add book context
This commit is contained in:
parent
358913f0ef
commit
dabf63d91d
@ -92,6 +92,10 @@ var tests = new (string Name, Action Test)[]
|
|||||||
("Story Intelligence enriches existing structural scenes", StoryIntelligenceEnrichesExistingStructuralScenes),
|
("Story Intelligence enriches existing structural scenes", StoryIntelligenceEnrichesExistingStructuralScenes),
|
||||||
("Core import scene titles ride existing structural AI call", CoreImportSceneTitlesRideExistingStructuralAiCall),
|
("Core import scene titles ride existing structural AI call", CoreImportSceneTitlesRideExistingStructuralAiCall),
|
||||||
("Core import scene title preference is review scoped", CoreImportSceneTitlePreferenceIsReviewScoped),
|
("Core import scene title preference is review scoped", CoreImportSceneTitlePreferenceIsReviewScoped),
|
||||||
|
("Core import scene title preference moved to Detect Scenes", CoreImportSceneTitlePreferenceMovedToDetectScenes),
|
||||||
|
("Book context is canonical import metadata", BookContextIsCanonicalImportMetadata),
|
||||||
|
("Names defaults from selected book context", NamesDefaultsFromSelectedBookContext),
|
||||||
|
("Story Intelligence starts from core import complete", StoryIntelligenceStartsFromCoreImportComplete),
|
||||||
("Generated scene titles persist with provenance", GeneratedSceneTitlesPersistWithProvenance),
|
("Generated scene titles persist with provenance", GeneratedSceneTitlesPersistWithProvenance),
|
||||||
("Scene title display keeps scene numbers visible", SceneTitleDisplayKeepsSceneNumbersVisible),
|
("Scene title display keeps scene numbers visible", SceneTitleDisplayKeepsSceneNumbersVisible),
|
||||||
("Phase 21T repairs durable character understanding", Phase21TRepairsDurableCharacterUnderstanding),
|
("Phase 21T repairs durable character understanding", Phase21TRepairsDurableCharacterUnderstanding),
|
||||||
@ -2537,14 +2541,74 @@ static void CoreImportSceneTitlePreferenceIsReviewScoped()
|
|||||||
var models = File.ReadAllText(Path.Combine(root, "Models/ManuscriptScanModels.cs"));
|
var models = File.ReadAllText(Path.Combine(root, "Models/ManuscriptScanModels.cs"));
|
||||||
var viewModels = File.ReadAllText(Path.Combine(root, "ViewModels/OnboardingViewModels.cs"));
|
var viewModels = File.ReadAllText(Path.Combine(root, "ViewModels/OnboardingViewModels.cs"));
|
||||||
var onboarding = File.ReadAllText(Path.Combine(root, "Services/OnboardingService.cs"));
|
var onboarding = File.ReadAllText(Path.Combine(root, "Services/OnboardingService.cs"));
|
||||||
var review = File.ReadAllText(Path.Combine(root, "Views/Onboarding/ScanReview.cshtml"));
|
|
||||||
|
|
||||||
Assert(models.Contains("public bool GenerateSceneTitles", StringComparison.Ordinal), "Saved scan review should persist the scene-title generation choice.");
|
Assert(models.Contains("public bool GenerateSceneTitles", StringComparison.Ordinal), "Saved scan review should persist the scene-title generation choice.");
|
||||||
Assert(viewModels.Contains("public bool GenerateSceneTitles", StringComparison.Ordinal), "Scan review form/view model should expose the scene-title option.");
|
Assert(viewModels.Contains("public bool GenerateSceneTitles", StringComparison.Ordinal), "Scan review form/view model should expose the scene-title option.");
|
||||||
Assert(review.Contains("Generate descriptive scene titles", StringComparison.Ordinal), "Chapter review should show the scene-title checkbox before detection.");
|
|
||||||
Assert(onboarding.Contains("Title = generateSceneTitles ? scene.SuggestedTitle ?? string.Empty : string.Empty", StringComparison.Ordinal), "Unchecked scene title generation should suppress persisted generated titles.");
|
Assert(onboarding.Contains("Title = generateSceneTitles ? scene.SuggestedTitle ?? string.Empty : string.Empty", StringComparison.Ordinal), "Unchecked scene title generation should suppress persisted generated titles.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void CoreImportSceneTitlePreferenceMovedToDetectScenes()
|
||||||
|
{
|
||||||
|
var root = Path.Combine(AppContext.BaseDirectory, "../../../../PlotLine");
|
||||||
|
var review = File.ReadAllText(Path.Combine(root, "Views/Onboarding/ScanReview.cshtml"));
|
||||||
|
var index = File.ReadAllText(Path.Combine(root, "Views/Onboarding/Index.cshtml"));
|
||||||
|
var script = File.ReadAllText(Path.Combine(root, "wwwroot/js/word-companion-presence.js"));
|
||||||
|
var hub = File.ReadAllText(Path.Combine(root, "Hubs/WordCompanionFollowHub.cs"));
|
||||||
|
|
||||||
|
Assert(!review.Contains("Generate descriptive scene titles", StringComparison.Ordinal), "Chapter review should not show the scene-title checkbox.");
|
||||||
|
Assert(index.Contains("data-onboarding-generate-scene-titles", StringComparison.Ordinal), "Detect Scenes should own the scene-title checkbox.");
|
||||||
|
Assert(script.Contains("StartOnboardingProjectBuild\", previewId, generateSceneTitles", StringComparison.Ordinal), "Detect Scenes should send the title preference when starting the build.");
|
||||||
|
Assert(hub.Contains("StartOnboardingProjectBuild(Guid previewId, bool generateSceneTitles = true)", StringComparison.Ordinal), "Build hub should accept the title preference without a separate AI call.");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void BookContextIsCanonicalImportMetadata()
|
||||||
|
{
|
||||||
|
var root = Path.Combine(AppContext.BaseDirectory, "../../../../PlotLine");
|
||||||
|
var model = File.ReadAllText(Path.Combine(root, "Models/CoreModels.cs"));
|
||||||
|
var viewModel = File.ReadAllText(Path.Combine(root, "ViewModels/CoreViewModels.cs"));
|
||||||
|
var edit = File.ReadAllText(Path.Combine(root, "Views/Books/Edit.cshtml"));
|
||||||
|
var onboarding = File.ReadAllText(Path.Combine(root, "Views/Onboarding/Index.cshtml"));
|
||||||
|
var repository = File.ReadAllText(Path.Combine(root, "Data/Repositories.cs"));
|
||||||
|
var migration = File.ReadAllText(Path.Combine(root, "Sql/163_Phase25H_BookContextAndLeanImportFlow.sql"));
|
||||||
|
|
||||||
|
Assert(model.Contains("public string? StoryEra", StringComparison.Ordinal), "Book should carry story era as central metadata.");
|
||||||
|
Assert(model.Contains("public string? PrimaryLocaleCode", StringComparison.Ordinal), "Book should carry primary locale as central metadata.");
|
||||||
|
Assert(viewModel.Contains("StoryEraOptions", StringComparison.Ordinal) && viewModel.Contains("PrimaryLocaleOptions", StringComparison.Ordinal), "Book edit view model should expose era and locale options.");
|
||||||
|
Assert(edit.Contains("asp-for=\"StoryEra\"", StringComparison.Ordinal) && edit.Contains("asp-for=\"PrimaryLocaleCode\"", StringComparison.Ordinal), "Book create/edit should expose era and primary locale.");
|
||||||
|
Assert(onboarding.Contains("name=\"StoryEra\"", StringComparison.Ordinal) && onboarding.Contains("name=\"PrimaryLocaleCode\"", StringComparison.Ordinal), "Onboarding book creation should capture era and primary locale.");
|
||||||
|
Assert(repository.Contains("book.StoryEra", StringComparison.Ordinal) && repository.Contains("book.PrimaryLocaleCode", StringComparison.Ordinal), "Book repository should pass context through stored procedures.");
|
||||||
|
Assert(migration.Contains("ALTER TABLE dbo.Books ADD StoryEra", StringComparison.Ordinal), "Book context migration should add StoryEra.");
|
||||||
|
Assert(migration.Contains("ALTER TABLE dbo.Books ADD PrimaryLocaleCode", StringComparison.Ordinal), "Book context migration should add PrimaryLocaleCode.");
|
||||||
|
Assert(migration.Contains("CREATE OR ALTER PROCEDURE dbo.Book_Save", StringComparison.Ordinal), "Book context migration should update Book_Save.");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void NamesDefaultsFromSelectedBookContext()
|
||||||
|
{
|
||||||
|
var root = Path.Combine(AppContext.BaseDirectory, "../../../../PlotLine");
|
||||||
|
var service = File.ReadAllText(Path.Combine(root, "Services/NameIntelligenceService.cs"));
|
||||||
|
|
||||||
|
Assert(service.Contains("ApplyBookDefaults(filter, projectBooks, locales)", StringComparison.Ordinal), "Names library should apply selected-book defaults before searching.");
|
||||||
|
Assert(service.Contains("filter.LocaleCode = book.PrimaryLocaleCode", StringComparison.Ordinal), "Names library should default locale from Book.PrimaryLocaleCode.");
|
||||||
|
Assert(service.Contains("BookContextOptions.RepresentativeBirthYear(book.StoryEra)", StringComparison.Ordinal), "Names library should default era as a representative birth year.");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void StoryIntelligenceStartsFromCoreImportComplete()
|
||||||
|
{
|
||||||
|
var root = Path.Combine(AppContext.BaseDirectory, "../../../../PlotLine");
|
||||||
|
var complete = File.ReadAllText(Path.Combine(root, "Views/Onboarding/BuildComplete.cshtml"));
|
||||||
|
var controller = File.ReadAllText(Path.Combine(root, "Controllers/OnboardingController.cs"));
|
||||||
|
var service = File.ReadAllText(Path.Combine(root, "Services/OnboardingStoryIntelligenceService.cs"));
|
||||||
|
|
||||||
|
Assert(complete.Contains("asp-action=\"StartStoryIntelligence\"", StringComparison.Ordinal), "Core Import Complete should start Story Intelligence directly.");
|
||||||
|
Assert(!complete.Contains("asp-action=\"StoryIntelligence\"", StringComparison.Ordinal), "Core Import Complete should not route through the redundant ready page.");
|
||||||
|
Assert(controller.Contains("public async Task<IActionResult> StoryIntelligence()\n {", StringComparison.Ordinal)
|
||||||
|
&& controller.Contains("return RedirectToAction(nameof(BuildComplete)", StringComparison.Ordinal), "Old ready route should be a compatibility redirect.");
|
||||||
|
Assert(controller.Contains("public async Task<IActionResult> StoryIntelligenceContext()\n {\n return await StoryIntelligence();", StringComparison.Ordinal), "Old context route should not remain an active wizard page.");
|
||||||
|
Assert(controller.Contains("var job = await storyIntelligence.StartAsync();", StringComparison.Ordinal), "Story Intelligence start should not require portrait/context form fields.");
|
||||||
|
Assert(service.Contains("BookContextOptions.StoryEraLabel(book?.StoryEra)", StringComparison.Ordinal), "Story Intelligence should derive era from Book context.");
|
||||||
|
Assert(service.Contains("BookContextOptions.PrimaryLocaleLabel(book?.PrimaryLocaleCode)", StringComparison.Ordinal), "Story Intelligence should derive locale from Book context.");
|
||||||
|
}
|
||||||
|
|
||||||
static void GeneratedSceneTitlesPersistWithProvenance()
|
static void GeneratedSceneTitlesPersistWithProvenance()
|
||||||
{
|
{
|
||||||
var root = Path.Combine(AppContext.BaseDirectory, "../../../../PlotLine");
|
var root = Path.Combine(AppContext.BaseDirectory, "../../../../PlotLine");
|
||||||
|
|||||||
@ -69,6 +69,8 @@ public sealed class BooksController(IBookService books, IOnboardingService onboa
|
|||||||
createModel.BookNumber = model.BookNumber;
|
createModel.BookNumber = model.BookNumber;
|
||||||
createModel.Description = model.Description;
|
createModel.Description = model.Description;
|
||||||
createModel.TargetWordCount = model.TargetWordCount;
|
createModel.TargetWordCount = model.TargetWordCount;
|
||||||
|
createModel.StoryEra = model.StoryEra;
|
||||||
|
createModel.PrimaryLocaleCode = model.PrimaryLocaleCode;
|
||||||
createModel.WritingStartDate = model.WritingStartDate;
|
createModel.WritingStartDate = model.WritingStartDate;
|
||||||
createModel.TargetCompletionDate = model.TargetCompletionDate;
|
createModel.TargetCompletionDate = model.TargetCompletionDate;
|
||||||
createModel.PublicationDate = model.PublicationDate;
|
createModel.PublicationDate = model.PublicationDate;
|
||||||
|
|||||||
@ -84,14 +84,23 @@ public sealed class OnboardingController(IOnboardingService onboarding, IOnboard
|
|||||||
public async Task<IActionResult> StoryIntelligence()
|
public async Task<IActionResult> StoryIntelligence()
|
||||||
{
|
{
|
||||||
var model = await storyIntelligence.GetOverviewAsync();
|
var model = await storyIntelligence.GetOverviewAsync();
|
||||||
return View(model);
|
if (model.ExistingBatchID.HasValue)
|
||||||
|
{
|
||||||
|
return RedirectToAction(nameof(StoryIntelligenceProgress), new { batchId = model.ExistingBatchID.Value });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (model.PreviewID == Guid.Empty)
|
||||||
|
{
|
||||||
|
return RedirectToAction(nameof(Index));
|
||||||
|
}
|
||||||
|
|
||||||
|
return RedirectToAction(nameof(BuildComplete), new { previewId = model.PreviewID });
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("story-intelligence/context")]
|
[HttpGet("story-intelligence/context")]
|
||||||
public async Task<IActionResult> StoryIntelligenceContext()
|
public async Task<IActionResult> StoryIntelligenceContext()
|
||||||
{
|
{
|
||||||
var model = await storyIntelligence.GetOverviewAsync();
|
return await StoryIntelligence();
|
||||||
return View(model);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("story-intelligence/book/{bookId:int}/continue")]
|
[HttpGet("story-intelligence/book/{bookId:int}/continue")]
|
||||||
@ -119,17 +128,13 @@ public sealed class OnboardingController(IOnboardingService onboarding, IOnboard
|
|||||||
|
|
||||||
[HttpPost("story-intelligence/start")]
|
[HttpPost("story-intelligence/start")]
|
||||||
[ValidateAntiForgeryToken]
|
[ValidateAntiForgeryToken]
|
||||||
public async Task<IActionResult> StartStoryIntelligence(
|
public async Task<IActionResult> StartStoryIntelligence()
|
||||||
[FromForm] string? appearancePreference,
|
|
||||||
[FromForm] string? primaryAgeGroup,
|
|
||||||
[FromForm] string? storyEra,
|
|
||||||
[FromForm] string? storyLocation)
|
|
||||||
{
|
{
|
||||||
var job = await storyIntelligence.StartAsync(appearancePreference, primaryAgeGroup, storyEra, storyLocation);
|
var job = await storyIntelligence.StartAsync();
|
||||||
if (job is null)
|
if (job is null)
|
||||||
{
|
{
|
||||||
TempData["OnboardingStoryIntelligenceError"] = "Review the manuscript scan before starting Story Intelligence.";
|
TempData["OnboardingStoryIntelligenceError"] = "Review the manuscript scan before starting Story Intelligence.";
|
||||||
return RedirectToAction(nameof(StoryIntelligenceContext));
|
return RedirectToAction(nameof(Index));
|
||||||
}
|
}
|
||||||
|
|
||||||
return RedirectToAction(nameof(StoryIntelligenceProgress), new { batchId = job.BatchID });
|
return RedirectToAction(nameof(StoryIntelligenceProgress), new { batchId = job.BatchID });
|
||||||
|
|||||||
@ -2352,6 +2352,8 @@ public sealed class BookRepository(ISqlConnectionFactory connectionFactory) : IB
|
|||||||
book.BookNumber,
|
book.BookNumber,
|
||||||
book.Description,
|
book.Description,
|
||||||
book.TargetWordCount,
|
book.TargetWordCount,
|
||||||
|
book.StoryEra,
|
||||||
|
book.PrimaryLocaleCode,
|
||||||
book.WritingStartDate,
|
book.WritingStartDate,
|
||||||
book.TargetCompletionDate,
|
book.TargetCompletionDate,
|
||||||
book.PublicationDate,
|
book.PublicationDate,
|
||||||
|
|||||||
@ -203,7 +203,7 @@ public sealed class WordCompanionFollowHub(
|
|||||||
return scanState;
|
return scanState;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<OnboardingManuscriptBuildResult> StartOnboardingProjectBuild(Guid previewId)
|
public async Task<OnboardingManuscriptBuildResult> StartOnboardingProjectBuild(Guid previewId, bool generateSceneTitles = true)
|
||||||
{
|
{
|
||||||
var userId = RequireUserId();
|
var userId = RequireUserId();
|
||||||
await Clients.Group(PresenceGroup(userId)).SendAsync("OnboardingBuildProgress", new OnboardingBuildProgress
|
await Clients.Group(PresenceGroup(userId)).SendAsync("OnboardingBuildProgress", new OnboardingBuildProgress
|
||||||
@ -219,6 +219,7 @@ public sealed class WordCompanionFollowHub(
|
|||||||
{
|
{
|
||||||
result = await onboardingService.BuildApprovedStructureAsync(
|
result = await onboardingService.BuildApprovedStructureAsync(
|
||||||
previewId,
|
previewId,
|
||||||
|
generateSceneTitles,
|
||||||
progress => Clients.Group(PresenceGroup(userId)).SendAsync("OnboardingBuildProgress", progress));
|
progress => Clients.Group(PresenceGroup(userId)).SendAsync("OnboardingBuildProgress", progress));
|
||||||
}
|
}
|
||||||
catch (InvalidOperationException ex)
|
catch (InvalidOperationException ex)
|
||||||
|
|||||||
@ -235,6 +235,8 @@ public sealed class Book
|
|||||||
public int BookNumber { get; set; }
|
public int BookNumber { get; set; }
|
||||||
public string? Description { get; set; }
|
public string? Description { get; set; }
|
||||||
public int? TargetWordCount { get; set; }
|
public int? TargetWordCount { get; set; }
|
||||||
|
public string? StoryEra { get; set; }
|
||||||
|
public string? PrimaryLocaleCode { get; set; }
|
||||||
public DateTime? WritingStartDate { get; set; }
|
public DateTime? WritingStartDate { get; set; }
|
||||||
public DateTime? TargetCompletionDate { get; set; }
|
public DateTime? TargetCompletionDate { get; set; }
|
||||||
public DateTime? PublicationDate { get; set; }
|
public DateTime? PublicationDate { get; set; }
|
||||||
@ -255,6 +257,53 @@ public sealed class Book
|
|||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public sealed record BookContextOption(string Value, string Label, int? RepresentativeBirthYear = null);
|
||||||
|
|
||||||
|
public static class BookContextOptions
|
||||||
|
{
|
||||||
|
public const string ContemporaryEra = "contemporary";
|
||||||
|
|
||||||
|
public static readonly IReadOnlyList<BookContextOption> StoryEras =
|
||||||
|
[
|
||||||
|
new("historical", "Historical"),
|
||||||
|
new("1900s", "1900s", 1905),
|
||||||
|
new("1910s", "1910s", 1915),
|
||||||
|
new("1920s", "1920s", 1925),
|
||||||
|
new("1930s", "1930s", 1935),
|
||||||
|
new("1940s", "1940s", 1945),
|
||||||
|
new("1950s", "1950s", 1955),
|
||||||
|
new("1960s", "1960s", 1965),
|
||||||
|
new("1970s", "1970s", 1975),
|
||||||
|
new("1980s", "1980s", 1985),
|
||||||
|
new("1990s", "1990s", 1995),
|
||||||
|
new("2000s", "2000s", 2005),
|
||||||
|
new("2010s", "2010s", 2015),
|
||||||
|
new("2020s", "2020s", 2025),
|
||||||
|
new(ContemporaryEra, "Contemporary"),
|
||||||
|
new("future", "Future"),
|
||||||
|
new("mixed-other", "Mixed / other")
|
||||||
|
];
|
||||||
|
|
||||||
|
public static readonly IReadOnlyList<BookContextOption> PrimaryLocales =
|
||||||
|
[
|
||||||
|
new("en-GB", "United Kingdom"),
|
||||||
|
new("en-IE", "Ireland"),
|
||||||
|
new("en-US", "United States"),
|
||||||
|
new("en-CA", "Canada"),
|
||||||
|
new("en-AU", "Australia"),
|
||||||
|
new("en-NZ", "New Zealand")
|
||||||
|
];
|
||||||
|
|
||||||
|
public static int? RepresentativeBirthYear(string? storyEra)
|
||||||
|
=> StoryEras.FirstOrDefault(option => string.Equals(option.Value, storyEra, StringComparison.OrdinalIgnoreCase))?.RepresentativeBirthYear;
|
||||||
|
|
||||||
|
public static string? StoryEraLabel(string? storyEra)
|
||||||
|
=> StoryEras.FirstOrDefault(option => string.Equals(option.Value, storyEra, StringComparison.OrdinalIgnoreCase))?.Label;
|
||||||
|
|
||||||
|
public static string? PrimaryLocaleLabel(string? localeCode)
|
||||||
|
=> PrimaryLocales.FirstOrDefault(option => string.Equals(option.Value, localeCode, StringComparison.OrdinalIgnoreCase))?.Label;
|
||||||
|
}
|
||||||
|
|
||||||
public sealed class ManuscriptDocumentModel
|
public sealed class ManuscriptDocumentModel
|
||||||
{
|
{
|
||||||
public int ManuscriptDocumentID { get; set; }
|
public int ManuscriptDocumentID { get; set; }
|
||||||
|
|||||||
@ -1212,7 +1212,9 @@ public sealed class BookService(
|
|||||||
BookNumber = existing.Count + 1,
|
BookNumber = existing.Count + 1,
|
||||||
Status = BookStatuses.Planning,
|
Status = BookStatuses.Planning,
|
||||||
CoverThumbnailPath = BookCoverService.PlaceholderPath,
|
CoverThumbnailPath = BookCoverService.PlaceholderPath,
|
||||||
StatusOptions = BuildStatusOptions(BookStatuses.Planning)
|
StatusOptions = BuildStatusOptions(BookStatuses.Planning),
|
||||||
|
StoryEraOptions = BuildStoryEraOptions(null),
|
||||||
|
PrimaryLocaleOptions = BuildPrimaryLocaleOptions(null)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1236,6 +1238,8 @@ public sealed class BookService(
|
|||||||
BookNumber = book.BookNumber,
|
BookNumber = book.BookNumber,
|
||||||
Description = book.Description,
|
Description = book.Description,
|
||||||
TargetWordCount = book.TargetWordCount,
|
TargetWordCount = book.TargetWordCount,
|
||||||
|
StoryEra = book.StoryEra,
|
||||||
|
PrimaryLocaleCode = book.PrimaryLocaleCode,
|
||||||
WritingStartDate = book.WritingStartDate,
|
WritingStartDate = book.WritingStartDate,
|
||||||
TargetCompletionDate = book.TargetCompletionDate,
|
TargetCompletionDate = book.TargetCompletionDate,
|
||||||
PublicationDate = book.PublicationDate,
|
PublicationDate = book.PublicationDate,
|
||||||
@ -1243,7 +1247,9 @@ public sealed class BookService(
|
|||||||
CoverThumbnailPath = book.CoverThumbnailPath ?? BookCoverService.PlaceholderPath,
|
CoverThumbnailPath = book.CoverThumbnailPath ?? BookCoverService.PlaceholderPath,
|
||||||
CoverStandardPath = book.CoverStandardPath,
|
CoverStandardPath = book.CoverStandardPath,
|
||||||
Project = await projects.GetAsync(book.ProjectID),
|
Project = await projects.GetAsync(book.ProjectID),
|
||||||
StatusOptions = BuildStatusOptions(book.Status)
|
StatusOptions = BuildStatusOptions(book.Status),
|
||||||
|
StoryEraOptions = BuildStoryEraOptions(book.StoryEra),
|
||||||
|
PrimaryLocaleOptions = BuildPrimaryLocaleOptions(book.PrimaryLocaleCode)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1251,6 +1257,8 @@ public sealed class BookService(
|
|||||||
{
|
{
|
||||||
model.Project = await projects.GetAsync(model.ProjectID);
|
model.Project = await projects.GetAsync(model.ProjectID);
|
||||||
model.StatusOptions = BuildStatusOptions(model.Status);
|
model.StatusOptions = BuildStatusOptions(model.Status);
|
||||||
|
model.StoryEraOptions = BuildStoryEraOptions(model.StoryEra);
|
||||||
|
model.PrimaryLocaleOptions = BuildPrimaryLocaleOptions(model.PrimaryLocaleCode);
|
||||||
if (model.BookID != 0)
|
if (model.BookID != 0)
|
||||||
{
|
{
|
||||||
var book = await books.GetAsync(model.BookID);
|
var book = await books.GetAsync(model.BookID);
|
||||||
@ -1284,6 +1292,8 @@ public sealed class BookService(
|
|||||||
BookNumber = model.BookNumber,
|
BookNumber = model.BookNumber,
|
||||||
Description = model.Description,
|
Description = model.Description,
|
||||||
TargetWordCount = model.TargetWordCount,
|
TargetWordCount = model.TargetWordCount,
|
||||||
|
StoryEra = TrimOptional(model.StoryEra),
|
||||||
|
PrimaryLocaleCode = TrimOptional(model.PrimaryLocaleCode),
|
||||||
WritingStartDate = model.WritingStartDate,
|
WritingStartDate = model.WritingStartDate,
|
||||||
TargetCompletionDate = model.TargetCompletionDate,
|
TargetCompletionDate = model.TargetCompletionDate,
|
||||||
PublicationDate = model.PublicationDate,
|
PublicationDate = model.PublicationDate,
|
||||||
@ -1309,6 +1319,31 @@ public sealed class BookService(
|
|||||||
return new(bookId, true);
|
return new(bookId, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string? TrimOptional(string? value)
|
||||||
|
{
|
||||||
|
var clean = value?.Trim();
|
||||||
|
return string.IsNullOrWhiteSpace(clean) ? null : clean;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IReadOnlyList<SelectListItem> BuildStoryEraOptions(string? selected)
|
||||||
|
=> BuildContextOptions(BookContextOptions.StoryEras, selected, "Not set");
|
||||||
|
|
||||||
|
private static IReadOnlyList<SelectListItem> BuildPrimaryLocaleOptions(string? selected)
|
||||||
|
=> BuildContextOptions(BookContextOptions.PrimaryLocales, selected, "Not set");
|
||||||
|
|
||||||
|
private static IReadOnlyList<SelectListItem> BuildContextOptions(IReadOnlyList<BookContextOption> options, string? selected, string emptyLabel)
|
||||||
|
{
|
||||||
|
var items = new List<SelectListItem>
|
||||||
|
{
|
||||||
|
new(emptyLabel, string.Empty, string.IsNullOrWhiteSpace(selected))
|
||||||
|
};
|
||||||
|
items.AddRange(options.Select(option => new SelectListItem(
|
||||||
|
option.Label,
|
||||||
|
option.Value,
|
||||||
|
string.Equals(option.Value, selected, StringComparison.OrdinalIgnoreCase))));
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
public Task<ManuscriptDocumentUnlinkResult?> UnlinkManuscriptAsync(int bookId)
|
public Task<ManuscriptDocumentUnlinkResult?> UnlinkManuscriptAsync(int bookId)
|
||||||
{
|
{
|
||||||
return currentUser.UserId is int userId
|
return currentUser.UserId is int userId
|
||||||
|
|||||||
@ -202,7 +202,9 @@ public sealed class ManuscriptStructureAnalysisService(
|
|||||||
bookId = request.BookID,
|
bookId = request.BookID,
|
||||||
chapterId = (int?)null,
|
chapterId = (int?)null,
|
||||||
chapterNumber = request.ChapterNumber,
|
chapterNumber = request.ChapterNumber,
|
||||||
sourceLabel = request.SourceLabel
|
sourceLabel = request.SourceLabel,
|
||||||
|
storyEra = request.StoryEra,
|
||||||
|
primaryLocale = request.PrimaryLocale
|
||||||
}, JsonOptions);
|
}, JsonOptions);
|
||||||
|
|
||||||
private static string BuildChapterPrompt(string promptTemplate, string chapterContextJson, string chapterText)
|
private static string BuildChapterPrompt(string promptTemplate, string chapterContextJson, string chapterText)
|
||||||
@ -452,6 +454,8 @@ public sealed class ManuscriptStructureAnalysisRequest
|
|||||||
public string TemporaryChapterKey { get; init; } = string.Empty;
|
public string TemporaryChapterKey { get; init; } = string.Empty;
|
||||||
public int ChapterNumber { get; init; }
|
public int ChapterNumber { get; init; }
|
||||||
public string SourceLabel { get; init; } = string.Empty;
|
public string SourceLabel { get; init; } = string.Empty;
|
||||||
|
public string? StoryEra { get; init; }
|
||||||
|
public string? PrimaryLocale { get; init; }
|
||||||
public string ChapterText { get; init; } = string.Empty;
|
public string ChapterText { get; init; } = string.Empty;
|
||||||
public IReadOnlyList<ManuscriptScanParagraphMap> ParagraphMap { get; init; } = [];
|
public IReadOnlyList<ManuscriptScanParagraphMap> ParagraphMap { get; init; } = [];
|
||||||
public IReadOnlyList<ManuscriptScanSourceParagraph> SourceParagraphs { get; init; } = [];
|
public IReadOnlyList<ManuscriptScanSourceParagraph> SourceParagraphs { get; init; } = [];
|
||||||
|
|||||||
@ -134,17 +134,45 @@ public sealed class NameIntelligenceService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
var projectBooks = await books.ListByProjectAsync(filter.ProjectID);
|
var projectBooks = await books.ListByProjectAsync(filter.ProjectID);
|
||||||
|
var locales = await ListLocalesAsync(cancellationToken);
|
||||||
|
ApplyBookDefaults(filter, projectBooks, locales);
|
||||||
var rows = await SearchAsync(filter, cancellationToken);
|
var rows = await SearchAsync(filter, cancellationToken);
|
||||||
return new NamesLibraryViewModel
|
return new NamesLibraryViewModel
|
||||||
{
|
{
|
||||||
Project = project,
|
Project = project,
|
||||||
Filter = filter,
|
Filter = filter,
|
||||||
Locales = await ListLocalesAsync(cancellationToken),
|
Locales = locales,
|
||||||
Books = projectBooks,
|
Books = projectBooks,
|
||||||
Results = rows
|
Results = rows
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void ApplyBookDefaults(NameSearchFilter filter, IReadOnlyList<Book> projectBooks, IReadOnlyList<NameLocale> locales)
|
||||||
|
{
|
||||||
|
if (!filter.BookID.HasValue)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var book = projectBooks.FirstOrDefault(item => item.BookID == filter.BookID.Value);
|
||||||
|
if (book is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(filter.LocaleCode)
|
||||||
|
&& !string.IsNullOrWhiteSpace(book.PrimaryLocaleCode)
|
||||||
|
&& locales.Any(locale => string.Equals(locale.LocaleCode, book.PrimaryLocaleCode, StringComparison.OrdinalIgnoreCase)))
|
||||||
|
{
|
||||||
|
filter.LocaleCode = book.PrimaryLocaleCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!filter.BirthYear.HasValue)
|
||||||
|
{
|
||||||
|
filter.BirthYear = BookContextOptions.RepresentativeBirthYear(book.StoryEra);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static string NormaliseName(string? value)
|
public static string NormaliseName(string? value)
|
||||||
=> string.IsNullOrWhiteSpace(value)
|
=> string.IsNullOrWhiteSpace(value)
|
||||||
? string.Empty
|
? string.Empty
|
||||||
|
|||||||
@ -12,7 +12,7 @@ public interface IOnboardingService
|
|||||||
Task<ManuscriptScanReviewViewModel?> GetScanReviewAsync(Guid? previewId = null);
|
Task<ManuscriptScanReviewViewModel?> GetScanReviewAsync(Guid? previewId = null);
|
||||||
Task<ManuscriptScanReviewViewModel?> SaveScanReviewAsync(ManuscriptScanReviewForm form, bool readyToImport);
|
Task<ManuscriptScanReviewViewModel?> SaveScanReviewAsync(ManuscriptScanReviewForm form, bool readyToImport);
|
||||||
Task<OnboardingManuscriptBuildResult?> FinaliseCharacterReviewAsync(ManuscriptScanReviewForm form);
|
Task<OnboardingManuscriptBuildResult?> FinaliseCharacterReviewAsync(ManuscriptScanReviewForm form);
|
||||||
Task<OnboardingManuscriptBuildResult?> BuildApprovedStructureAsync(Guid previewId, Func<OnboardingBuildProgress, Task>? progress = null);
|
Task<OnboardingManuscriptBuildResult?> BuildApprovedStructureAsync(Guid previewId, bool generateSceneTitles = true, Func<OnboardingBuildProgress, Task>? progress = null);
|
||||||
Task<OnboardingManuscriptBuildResult?> GetBuildResultAsync(Guid previewId);
|
Task<OnboardingManuscriptBuildResult?> GetBuildResultAsync(Guid previewId);
|
||||||
Task<bool> ShouldShowOnboardingAsync();
|
Task<bool> ShouldShowOnboardingAsync();
|
||||||
Task<OnboardingNudgeViewModel> GetDashboardNudgeAsync();
|
Task<OnboardingNudgeViewModel> GetDashboardNudgeAsync();
|
||||||
@ -103,7 +103,9 @@ public sealed class OnboardingService(
|
|||||||
BookForm = new BookOnboardingForm
|
BookForm = new BookOnboardingForm
|
||||||
{
|
{
|
||||||
Mode = bookOptions.Count == 0 || !state.BookID.HasValue ? OnboardingSelectionModes.Create : OnboardingSelectionModes.Select,
|
Mode = bookOptions.Count == 0 || !state.BookID.HasValue ? OnboardingSelectionModes.Create : OnboardingSelectionModes.Select,
|
||||||
SelectedBookID = state.BookID
|
SelectedBookID = state.BookID,
|
||||||
|
StoryEra = selectedBook?.StoryEra,
|
||||||
|
PrimaryLocaleCode = selectedBook?.PrimaryLocaleCode
|
||||||
},
|
},
|
||||||
Options = step switch
|
Options = step switch
|
||||||
{
|
{
|
||||||
@ -225,7 +227,7 @@ public sealed class OnboardingService(
|
|||||||
return saved is null ? null : BuildScanReviewViewModel((userId, preview, saved, project, book, existingCharacters, build));
|
return saved is null ? null : BuildScanReviewViewModel((userId, preview, saved, project, book, existingCharacters, build));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<OnboardingManuscriptBuildResult?> BuildApprovedStructureAsync(Guid previewId, Func<OnboardingBuildProgress, Task>? progress = null)
|
public async Task<OnboardingManuscriptBuildResult?> BuildApprovedStructureAsync(Guid previewId, bool generateSceneTitles = true, Func<OnboardingBuildProgress, Task>? progress = null)
|
||||||
{
|
{
|
||||||
var context = await GetScanReviewContextAsync(previewId);
|
var context = await GetScanReviewContextAsync(previewId);
|
||||||
if (context is null)
|
if (context is null)
|
||||||
@ -233,13 +235,26 @@ public sealed class OnboardingService(
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var (userId, preview, review, _, _, _, _) = context.Value;
|
var (userId, preview, review, _, book, _, _) = context.Value;
|
||||||
var generateSceneTitles = review.GenerateSceneTitles;
|
|
||||||
if (!string.Equals(review.Status, ManuscriptScanReviewStatuses.ReadyToImport, StringComparison.Ordinal))
|
if (!string.Equals(review.Status, ManuscriptScanReviewStatuses.ReadyToImport, StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException("Review the scan and choose Save and continue before preparing chapters.");
|
throw new InvalidOperationException("Review the scan and choose Save and continue before preparing chapters.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (review.GenerateSceneTitles != generateSceneTitles)
|
||||||
|
{
|
||||||
|
review = new ManuscriptScanReviewDecision
|
||||||
|
{
|
||||||
|
PreviewID = review.PreviewID,
|
||||||
|
Status = review.Status,
|
||||||
|
GenerateSceneTitles = generateSceneTitles,
|
||||||
|
Chapters = review.Chapters,
|
||||||
|
Scenes = review.Scenes,
|
||||||
|
Characters = review.Characters
|
||||||
|
};
|
||||||
|
await scanPreviews.SaveReviewAsync(userId, review);
|
||||||
|
}
|
||||||
|
|
||||||
var existingBuild = await scanPreviews.GetBuildResultAsync(userId, preview.PreviewID)
|
var existingBuild = await scanPreviews.GetBuildResultAsync(userId, preview.PreviewID)
|
||||||
?? await builds.GetByPreviewAsync(userId, preview.PreviewID);
|
?? await builds.GetByPreviewAsync(userId, preview.PreviewID);
|
||||||
if (existingBuild is not null && existingBuild.SceneMappings.Count > 0)
|
if (existingBuild is not null && existingBuild.SceneMappings.Count > 0)
|
||||||
@ -287,6 +302,8 @@ public sealed class OnboardingService(
|
|||||||
TemporaryChapterKey = chapterDecision.TemporaryChapterKey,
|
TemporaryChapterKey = chapterDecision.TemporaryChapterKey,
|
||||||
ChapterNumber = chapterDecision.ChapterNumber,
|
ChapterNumber = chapterDecision.ChapterNumber,
|
||||||
SourceLabel = $"{preview.DocumentTitle ?? "Word manuscript"}: {chapterDecision.Title}",
|
SourceLabel = $"{preview.DocumentTitle ?? "Word manuscript"}: {chapterDecision.Title}",
|
||||||
|
StoryEra = BookContextOptions.StoryEraLabel(book.StoryEra),
|
||||||
|
PrimaryLocale = BookContextOptions.PrimaryLocaleLabel(book.PrimaryLocaleCode),
|
||||||
ChapterText = chapterPreview.ChapterText,
|
ChapterText = chapterPreview.ChapterText,
|
||||||
ParagraphMap = chapterPreview.ParagraphMap,
|
ParagraphMap = chapterPreview.ParagraphMap,
|
||||||
SourceParagraphs = chapterPreview.SourceParagraphs
|
SourceParagraphs = chapterPreview.SourceParagraphs
|
||||||
@ -1255,6 +1272,8 @@ public sealed class OnboardingService(
|
|||||||
createModel.BookTitle = bookTitle;
|
createModel.BookTitle = bookTitle;
|
||||||
createModel.Subtitle = CleanOptional(form.Subtitle);
|
createModel.Subtitle = CleanOptional(form.Subtitle);
|
||||||
createModel.TargetWordCount = form.TargetWordCount;
|
createModel.TargetWordCount = form.TargetWordCount;
|
||||||
|
createModel.StoryEra = CleanOptional(form.StoryEra);
|
||||||
|
createModel.PrimaryLocaleCode = CleanOptional(form.PrimaryLocaleCode);
|
||||||
var result = await bookService.SaveAsync(createModel);
|
var result = await bookService.SaveAsync(createModel);
|
||||||
if (!result.Succeeded)
|
if (!result.Succeeded)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -45,6 +45,7 @@ public sealed class OnboardingStoryIntelligenceService(
|
|||||||
IStoryIntelligenceSourceRepository sources,
|
IStoryIntelligenceSourceRepository sources,
|
||||||
IStoryIntelligencePipelineStateService pipelineState,
|
IStoryIntelligencePipelineStateService pipelineState,
|
||||||
IStoryMemoryRepository storyMemory,
|
IStoryMemoryRepository storyMemory,
|
||||||
|
IBookRepository books,
|
||||||
IStoryIntelligenceClient client,
|
IStoryIntelligenceClient client,
|
||||||
IOnboardingStoryIntelligenceBatchStore batchStore,
|
IOnboardingStoryIntelligenceBatchStore batchStore,
|
||||||
IStoryIntelligenceProgressNotifier notifier,
|
IStoryIntelligenceProgressNotifier notifier,
|
||||||
@ -106,6 +107,7 @@ public sealed class OnboardingStoryIntelligenceService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
var (state, preview, review, build, wizard) = context.Value;
|
var (state, preview, review, build, wizard) = context.Value;
|
||||||
|
var book = await books.GetAsync(preview.BookID);
|
||||||
var existing = await batchStore.GetLatestForPreviewAsync(userId, preview.PreviewID);
|
var existing = await batchStore.GetLatestForPreviewAsync(userId, preview.PreviewID);
|
||||||
if (existing is not null)
|
if (existing is not null)
|
||||||
{
|
{
|
||||||
@ -215,8 +217,12 @@ public sealed class OnboardingStoryIntelligenceService(
|
|||||||
BookTitle = wizard.SelectedBookTitle,
|
BookTitle = wizard.SelectedBookTitle,
|
||||||
AppearancePreference = NormaliseAppearancePreference(appearancePreference),
|
AppearancePreference = NormaliseAppearancePreference(appearancePreference),
|
||||||
PrimaryAgeGroup = CleanContext(primaryAgeGroup),
|
PrimaryAgeGroup = CleanContext(primaryAgeGroup),
|
||||||
StoryEra = CleanContext(storyEra),
|
StoryEra = CleanContext(storyEra) is { Length: > 0 } suppliedEra
|
||||||
StoryLocation = CleanContext(storyLocation),
|
? suppliedEra
|
||||||
|
: CleanContext(BookContextOptions.StoryEraLabel(book?.StoryEra)),
|
||||||
|
StoryLocation = CleanContext(storyLocation) is { Length: > 0 } suppliedLocation
|
||||||
|
? suppliedLocation
|
||||||
|
: CleanContext(BookContextOptions.PrimaryLocaleLabel(book?.PrimaryLocaleCode)),
|
||||||
Items = batchItems
|
Items = batchItems
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
198
PlotLine/Sql/163_Phase25H_BookContextAndLeanImportFlow.sql
Normal file
198
PlotLine/Sql/163_Phase25H_BookContextAndLeanImportFlow.sql
Normal file
@ -0,0 +1,198 @@
|
|||||||
|
SET NOCOUNT ON;
|
||||||
|
GO
|
||||||
|
|
||||||
|
IF COL_LENGTH('dbo.Books', 'StoryEra') IS NULL
|
||||||
|
BEGIN
|
||||||
|
ALTER TABLE dbo.Books ADD StoryEra nvarchar(80) NULL;
|
||||||
|
END;
|
||||||
|
GO
|
||||||
|
|
||||||
|
IF COL_LENGTH('dbo.Books', 'PrimaryLocaleCode') IS NULL
|
||||||
|
BEGIN
|
||||||
|
ALTER TABLE dbo.Books ADD PrimaryLocaleCode nvarchar(16) NULL;
|
||||||
|
END;
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE OR ALTER PROCEDURE dbo.Book_ListByProject
|
||||||
|
@ProjectID int
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
|
||||||
|
SELECT b.BookID, b.ProjectID, b.BookTitle, b.Subtitle, b.Tagline, b.ShortDescription, b.Status,
|
||||||
|
b.BookNumber, b.Description, b.TargetWordCount, b.StoryEra, b.PrimaryLocaleCode,
|
||||||
|
b.WritingStartDate, b.TargetCompletionDate, b.PublicationDate, b.UsesExplicitScenes,
|
||||||
|
b.CoverThumbnailPath, b.CoverStandardPath,
|
||||||
|
ISNULL(wordCounts.CurrentWordCount, 0) AS CurrentWordCount,
|
||||||
|
ISNULL(counts.ChapterCount, 0) AS ChapterCount,
|
||||||
|
ISNULL(counts.SceneCount, 0) AS SceneCount,
|
||||||
|
b.SortOrder, b.CreatedDate, b.UpdatedDate, b.IsArchived, b.ArchivedDate, b.ArchivedReason
|
||||||
|
FROM dbo.Books b
|
||||||
|
OUTER APPLY
|
||||||
|
(
|
||||||
|
SELECT COUNT(DISTINCT c.ChapterID) AS ChapterCount, COUNT(s.SceneID) AS SceneCount
|
||||||
|
FROM dbo.Chapters c
|
||||||
|
LEFT JOIN dbo.Scenes s ON s.ChapterID = c.ChapterID AND s.IsArchived = 0
|
||||||
|
WHERE c.BookID = b.BookID AND c.IsArchived = 0
|
||||||
|
) counts
|
||||||
|
OUTER APPLY
|
||||||
|
(
|
||||||
|
SELECT SUM(ISNULL(sw.ActualWordCount, 0)) AS CurrentWordCount
|
||||||
|
FROM dbo.Chapters c
|
||||||
|
INNER JOIN dbo.Scenes s ON s.ChapterID = c.ChapterID AND s.IsArchived = 0
|
||||||
|
LEFT JOIN dbo.SceneWorkflow sw ON sw.SceneID = s.SceneID
|
||||||
|
WHERE c.BookID = b.BookID AND c.IsArchived = 0
|
||||||
|
) wordCounts
|
||||||
|
WHERE b.ProjectID = @ProjectID AND b.IsArchived = 0
|
||||||
|
ORDER BY b.SortOrder, b.BookNumber, b.BookTitle;
|
||||||
|
END;
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE OR ALTER PROCEDURE dbo.Book_ListByProjects
|
||||||
|
@ProjectIDs nvarchar(max)
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
|
||||||
|
;WITH ProjectIDs AS
|
||||||
|
(
|
||||||
|
SELECT DISTINCT TRY_CONVERT(int, value) AS ProjectID
|
||||||
|
FROM STRING_SPLIT(COALESCE(@ProjectIDs, N''), N',')
|
||||||
|
WHERE TRY_CONVERT(int, value) IS NOT NULL
|
||||||
|
)
|
||||||
|
SELECT b.BookID, b.ProjectID, b.BookTitle, b.Subtitle, b.Tagline, b.ShortDescription, b.Status,
|
||||||
|
b.BookNumber, b.Description, b.TargetWordCount, b.StoryEra, b.PrimaryLocaleCode,
|
||||||
|
b.WritingStartDate, b.TargetCompletionDate, b.PublicationDate, b.UsesExplicitScenes,
|
||||||
|
b.CoverThumbnailPath, b.CoverStandardPath,
|
||||||
|
ISNULL(wordCounts.CurrentWordCount, 0) AS CurrentWordCount,
|
||||||
|
ISNULL(counts.ChapterCount, 0) AS ChapterCount,
|
||||||
|
ISNULL(counts.SceneCount, 0) AS SceneCount,
|
||||||
|
b.SortOrder, b.CreatedDate, b.UpdatedDate, b.IsArchived, b.ArchivedDate, b.ArchivedReason
|
||||||
|
FROM dbo.Books b
|
||||||
|
INNER JOIN ProjectIDs ids ON ids.ProjectID = b.ProjectID
|
||||||
|
OUTER APPLY
|
||||||
|
(
|
||||||
|
SELECT COUNT(DISTINCT c.ChapterID) AS ChapterCount, COUNT(s.SceneID) AS SceneCount
|
||||||
|
FROM dbo.Chapters c
|
||||||
|
LEFT JOIN dbo.Scenes s ON s.ChapterID = c.ChapterID AND s.IsArchived = 0
|
||||||
|
WHERE c.BookID = b.BookID AND c.IsArchived = 0
|
||||||
|
) counts
|
||||||
|
OUTER APPLY
|
||||||
|
(
|
||||||
|
SELECT SUM(ISNULL(sw.ActualWordCount, 0)) AS CurrentWordCount
|
||||||
|
FROM dbo.Chapters c
|
||||||
|
INNER JOIN dbo.Scenes s ON s.ChapterID = c.ChapterID AND s.IsArchived = 0
|
||||||
|
LEFT JOIN dbo.SceneWorkflow sw ON sw.SceneID = s.SceneID
|
||||||
|
WHERE c.BookID = b.BookID AND c.IsArchived = 0
|
||||||
|
) wordCounts
|
||||||
|
WHERE b.IsArchived = 0
|
||||||
|
ORDER BY b.ProjectID, b.SortOrder, b.BookNumber, b.BookTitle;
|
||||||
|
END;
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE OR ALTER PROCEDURE dbo.Book_Get
|
||||||
|
@BookID int
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
|
||||||
|
SELECT b.BookID, b.ProjectID, b.BookTitle, b.Subtitle, b.Tagline, b.ShortDescription, b.Status,
|
||||||
|
b.BookNumber, b.Description, b.TargetWordCount, b.StoryEra, b.PrimaryLocaleCode,
|
||||||
|
b.WritingStartDate, b.TargetCompletionDate, b.PublicationDate, b.UsesExplicitScenes,
|
||||||
|
b.CoverThumbnailPath, b.CoverStandardPath,
|
||||||
|
ISNULL(wordCounts.CurrentWordCount, 0) AS CurrentWordCount,
|
||||||
|
ISNULL(counts.ChapterCount, 0) AS ChapterCount,
|
||||||
|
ISNULL(counts.SceneCount, 0) AS SceneCount,
|
||||||
|
b.SortOrder, b.CreatedDate, b.UpdatedDate, b.IsArchived, b.ArchivedDate, b.ArchivedReason
|
||||||
|
FROM dbo.Books b
|
||||||
|
OUTER APPLY
|
||||||
|
(
|
||||||
|
SELECT COUNT(DISTINCT c.ChapterID) AS ChapterCount, COUNT(s.SceneID) AS SceneCount
|
||||||
|
FROM dbo.Chapters c
|
||||||
|
LEFT JOIN dbo.Scenes s ON s.ChapterID = c.ChapterID AND s.IsArchived = 0
|
||||||
|
WHERE c.BookID = b.BookID AND c.IsArchived = 0
|
||||||
|
) counts
|
||||||
|
OUTER APPLY
|
||||||
|
(
|
||||||
|
SELECT SUM(ISNULL(sw.ActualWordCount, 0)) AS CurrentWordCount
|
||||||
|
FROM dbo.Chapters c
|
||||||
|
INNER JOIN dbo.Scenes s ON s.ChapterID = c.ChapterID AND s.IsArchived = 0
|
||||||
|
LEFT JOIN dbo.SceneWorkflow sw ON sw.SceneID = s.SceneID
|
||||||
|
WHERE c.BookID = b.BookID AND c.IsArchived = 0
|
||||||
|
) wordCounts
|
||||||
|
WHERE b.BookID = @BookID AND b.IsArchived = 0;
|
||||||
|
END;
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE OR ALTER PROCEDURE dbo.Book_Save
|
||||||
|
@BookID int = NULL,
|
||||||
|
@ProjectID int,
|
||||||
|
@BookTitle nvarchar(200),
|
||||||
|
@Subtitle nvarchar(200) = NULL,
|
||||||
|
@Tagline nvarchar(300) = NULL,
|
||||||
|
@ShortDescription nvarchar(max) = NULL,
|
||||||
|
@Status nvarchar(50) = N'Planning',
|
||||||
|
@BookNumber int,
|
||||||
|
@Description nvarchar(max) = NULL,
|
||||||
|
@TargetWordCount int = NULL,
|
||||||
|
@StoryEra nvarchar(80) = NULL,
|
||||||
|
@PrimaryLocaleCode nvarchar(16) = NULL,
|
||||||
|
@WritingStartDate date = NULL,
|
||||||
|
@TargetCompletionDate date = NULL,
|
||||||
|
@PublicationDate date = NULL,
|
||||||
|
@UsesExplicitScenes bit = 0
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
|
||||||
|
IF @TargetWordCount IS NOT NULL AND @TargetWordCount <= 0
|
||||||
|
THROW 51000, 'Target word count must be greater than zero.', 1;
|
||||||
|
|
||||||
|
SET @StoryEra = NULLIF(LTRIM(RTRIM(@StoryEra)), N'');
|
||||||
|
SET @PrimaryLocaleCode = NULLIF(LTRIM(RTRIM(@PrimaryLocaleCode)), N'');
|
||||||
|
|
||||||
|
IF NULLIF(LTRIM(RTRIM(@Status)), N'') IS NULL
|
||||||
|
SET @Status = N'Planning';
|
||||||
|
|
||||||
|
IF @BookID IS NULL OR @BookID = 0
|
||||||
|
BEGIN
|
||||||
|
DECLARE @NextBookOrder int = ISNULL((SELECT MAX(SortOrder) FROM dbo.Books WHERE ProjectID = @ProjectID), 0) + 10;
|
||||||
|
|
||||||
|
INSERT dbo.Books
|
||||||
|
(
|
||||||
|
ProjectID, BookTitle, Subtitle, Tagline, ShortDescription, Status, BookNumber, Description,
|
||||||
|
TargetWordCount, StoryEra, PrimaryLocaleCode, WritingStartDate, TargetCompletionDate,
|
||||||
|
PublicationDate, UsesExplicitScenes, SortOrder
|
||||||
|
)
|
||||||
|
VALUES
|
||||||
|
(
|
||||||
|
@ProjectID, @BookTitle, @Subtitle, @Tagline, @ShortDescription, @Status, @BookNumber, @Description,
|
||||||
|
@TargetWordCount, @StoryEra, @PrimaryLocaleCode, @WritingStartDate, @TargetCompletionDate,
|
||||||
|
@PublicationDate, @UsesExplicitScenes, @NextBookOrder
|
||||||
|
);
|
||||||
|
|
||||||
|
SELECT CAST(SCOPE_IDENTITY() AS int) AS BookID;
|
||||||
|
RETURN;
|
||||||
|
END;
|
||||||
|
|
||||||
|
UPDATE dbo.Books
|
||||||
|
SET BookTitle = @BookTitle,
|
||||||
|
Subtitle = @Subtitle,
|
||||||
|
Tagline = @Tagline,
|
||||||
|
ShortDescription = @ShortDescription,
|
||||||
|
Status = @Status,
|
||||||
|
BookNumber = @BookNumber,
|
||||||
|
Description = @Description,
|
||||||
|
TargetWordCount = @TargetWordCount,
|
||||||
|
StoryEra = @StoryEra,
|
||||||
|
PrimaryLocaleCode = @PrimaryLocaleCode,
|
||||||
|
WritingStartDate = @WritingStartDate,
|
||||||
|
TargetCompletionDate = @TargetCompletionDate,
|
||||||
|
PublicationDate = @PublicationDate,
|
||||||
|
UsesExplicitScenes = @UsesExplicitScenes,
|
||||||
|
UpdatedDate = SYSUTCDATETIME()
|
||||||
|
WHERE BookID = @BookID;
|
||||||
|
|
||||||
|
SELECT @BookID AS BookID;
|
||||||
|
END;
|
||||||
|
GO
|
||||||
@ -206,6 +206,14 @@ public sealed class BookEditViewModel
|
|||||||
[Range(1, int.MaxValue, ErrorMessage = "Target word count must be greater than zero.")]
|
[Range(1, int.MaxValue, ErrorMessage = "Target word count must be greater than zero.")]
|
||||||
public int? TargetWordCount { get; set; }
|
public int? TargetWordCount { get; set; }
|
||||||
|
|
||||||
|
[StringLength(80)]
|
||||||
|
[Display(Name = "Story era")]
|
||||||
|
public string? StoryEra { get; set; }
|
||||||
|
|
||||||
|
[StringLength(16)]
|
||||||
|
[Display(Name = "Primary locale")]
|
||||||
|
public string? PrimaryLocaleCode { get; set; }
|
||||||
|
|
||||||
[DataType(DataType.Date)]
|
[DataType(DataType.Date)]
|
||||||
[Display(Name = "Writing start date")]
|
[Display(Name = "Writing start date")]
|
||||||
public DateTime? WritingStartDate { get; set; }
|
public DateTime? WritingStartDate { get; set; }
|
||||||
@ -230,6 +238,8 @@ public sealed class BookEditViewModel
|
|||||||
public string? Description { get; set; }
|
public string? Description { get; set; }
|
||||||
public Project? Project { get; set; }
|
public Project? Project { get; set; }
|
||||||
public IReadOnlyList<SelectListItem> StatusOptions { get; set; } = [];
|
public IReadOnlyList<SelectListItem> StatusOptions { get; set; } = [];
|
||||||
|
public IReadOnlyList<SelectListItem> StoryEraOptions { get; set; } = [];
|
||||||
|
public IReadOnlyList<SelectListItem> PrimaryLocaleOptions { get; set; } = [];
|
||||||
public string BeginMode { get; set; } = BookBeginModes.Manual;
|
public string BeginMode { get; set; } = BookBeginModes.Manual;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -91,6 +91,12 @@ public sealed class BookOnboardingForm
|
|||||||
|
|
||||||
[Range(1, int.MaxValue, ErrorMessage = "Target word count must be greater than zero.")]
|
[Range(1, int.MaxValue, ErrorMessage = "Target word count must be greater than zero.")]
|
||||||
public int? TargetWordCount { get; set; }
|
public int? TargetWordCount { get; set; }
|
||||||
|
|
||||||
|
[StringLength(80)]
|
||||||
|
public string? StoryEra { get; set; }
|
||||||
|
|
||||||
|
[StringLength(16)]
|
||||||
|
public string? PrimaryLocaleCode { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class OnboardingProjectOptionViewModel
|
public sealed class OnboardingProjectOptionViewModel
|
||||||
|
|||||||
@ -95,6 +95,16 @@
|
|||||||
<input asp-for="TargetWordCount" class="form-control" min="1" />
|
<input asp-for="TargetWordCount" class="form-control" min="1" />
|
||||||
<span asp-validation-for="TargetWordCount" class="text-danger"></span>
|
<span asp-validation-for="TargetWordCount" class="text-danger"></span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label asp-for="StoryEra" class="form-label"></label>
|
||||||
|
<select asp-for="StoryEra" asp-items="Model.StoryEraOptions" class="form-select"></select>
|
||||||
|
<span asp-validation-for="StoryEra" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label asp-for="PrimaryLocaleCode" class="form-label"></label>
|
||||||
|
<select asp-for="PrimaryLocaleCode" asp-items="Model.PrimaryLocaleOptions" class="form-select"></select>
|
||||||
|
<span asp-validation-for="PrimaryLocaleCode" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<label asp-for="WritingStartDate" class="form-label"></label>
|
<label asp-for="WritingStartDate" class="form-label"></label>
|
||||||
<input asp-for="WritingStartDate" class="form-control" />
|
<input asp-for="WritingStartDate" class="form-control" />
|
||||||
|
|||||||
@ -111,7 +111,9 @@
|
|||||||
<div class="onboarding-actions">
|
<div class="onboarding-actions">
|
||||||
@if (coreComplete)
|
@if (coreComplete)
|
||||||
{
|
{
|
||||||
<a class="btn btn-primary" asp-controller="Onboarding" asp-action="StoryIntelligence">Analyse my book</a>
|
<form asp-controller="Onboarding" asp-action="StartStoryIntelligence" method="post">
|
||||||
|
<button class="btn btn-primary" type="submit">Analyse my book</button>
|
||||||
|
</form>
|
||||||
<a class="btn btn-outline-secondary" asp-controller="Projects" asp-action="Details" asp-route-id="@Model.ProjectID">Not now</a>
|
<a class="btn btn-outline-secondary" asp-controller="Projects" asp-action="Details" asp-route-id="@Model.ProjectID">Not now</a>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|||||||
@ -207,6 +207,28 @@
|
|||||||
<input class="form-control" id="TargetWordCount" name="TargetWordCount" type="number" min="1" value="@Model.BookForm.TargetWordCount" />
|
<input class="form-control" id="TargetWordCount" name="TargetWordCount" type="number" min="1" value="@Model.BookForm.TargetWordCount" />
|
||||||
@Html.ValidationMessage("TargetWordCount", null, new { @class = "text-danger" })
|
@Html.ValidationMessage("TargetWordCount", null, new { @class = "text-danger" })
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-md-5">
|
||||||
|
<label class="form-label" for="StoryEra">Story era</label>
|
||||||
|
<select class="form-select" id="StoryEra" name="StoryEra">
|
||||||
|
<option value="" selected="@string.IsNullOrWhiteSpace(Model.BookForm.StoryEra)">Not set</option>
|
||||||
|
@foreach (var option in BookContextOptions.StoryEras)
|
||||||
|
{
|
||||||
|
<option value="@option.Value" selected="@(string.Equals(Model.BookForm.StoryEra, option.Value, StringComparison.OrdinalIgnoreCase))">@option.Label</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
@Html.ValidationMessage("StoryEra", null, new { @class = "text-danger" })
|
||||||
|
</div>
|
||||||
|
<div class="col-md-5">
|
||||||
|
<label class="form-label" for="PrimaryLocaleCode">Primary locale</label>
|
||||||
|
<select class="form-select" id="PrimaryLocaleCode" name="PrimaryLocaleCode">
|
||||||
|
<option value="" selected="@string.IsNullOrWhiteSpace(Model.BookForm.PrimaryLocaleCode)">Not set</option>
|
||||||
|
@foreach (var option in BookContextOptions.PrimaryLocales)
|
||||||
|
{
|
||||||
|
<option value="@option.Value" selected="@(string.Equals(Model.BookForm.PrimaryLocaleCode, option.Value, StringComparison.OrdinalIgnoreCase))">@option.Label</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
@Html.ValidationMessage("PrimaryLocaleCode", null, new { @class = "text-danger" })
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@ -253,34 +275,72 @@
|
|||||||
</div>
|
</div>
|
||||||
@if (Model.IsMicrosoftWordPath)
|
@if (Model.IsMicrosoftWordPath)
|
||||||
{
|
{
|
||||||
<section class="onboarding-companion-card onboarding-companion-card--compact @(Model.CompanionPresence.IsConnected ? "is-connected" : "is-offline")"
|
@if (structureReadyToImport)
|
||||||
data-word-companion-presence
|
{
|
||||||
data-word-companion-status="@Model.CompanionPresence.Status"
|
<section class="onboarding-detect-scenes"
|
||||||
aria-label="Word Companion connection">
|
data-word-companion-presence
|
||||||
<div class="onboarding-companion-mark" aria-hidden="true">
|
data-word-companion-status="@Model.CompanionPresence.Status"
|
||||||
<span></span>
|
aria-label="Scene detection">
|
||||||
</div>
|
<p class="onboarding-compact-status">
|
||||||
<div class="onboarding-companion-body">
|
<span data-word-companion-waiting-status>@(Model.CompanionPresence.IsConnected ? "Word connected" : "Waiting for Word")</span>
|
||||||
<p class="eyebrow">Word Companion</p>
|
· @Model.ScanState.ChapterCount.ToString("N0") chapter@(Model.ScanState.ChapterCount == 1 ? string.Empty : "s")
|
||||||
<h2 data-word-companion-waiting-status>@(Model.CompanionPresence.IsConnected ? "Word Companion Connected" : "Waiting for Word Companion...")</h2>
|
· @Model.ScanState.TotalWordCount.ToString("N0") words
|
||||||
<p data-word-companion-connected-copy>
|
|
||||||
@CompanionScanMessage(Model.CompanionPresence, Model.ScanState)
|
|
||||||
</p>
|
</p>
|
||||||
<dl class="onboarding-companion-details">
|
<label class="onboarding-inline-choice">
|
||||||
|
<input type="checkbox" data-onboarding-generate-scene-titles checked />
|
||||||
|
<span>
|
||||||
|
<strong>Generate descriptive scene titles</strong>
|
||||||
|
<small>Short working titles used only within PlotDirector.</small>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<p>Keep Word open until scene markers have been added. If Word cannot save automatically, PlotDirector will ask you to save and confirm.</p>
|
||||||
|
<div class="onboarding-build-panel" data-onboarding-build-panel>
|
||||||
<div>
|
<div>
|
||||||
<dt>Version</dt>
|
<p class="onboarding-scan-next">Ready to detect scenes.</p>
|
||||||
<dd data-word-companion-version>@(Model.CompanionPresence.CompanionVersion ?? "Waiting")</dd>
|
<p class="onboarding-build-message" data-onboarding-build-message>Scene detection has not started yet.</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<strong data-onboarding-build-percent></strong>
|
||||||
<dt>Document</dt>
|
<div class="onboarding-scan-progress onboarding-build-progress" aria-hidden="true">
|
||||||
<dd data-word-companion-document>@(Model.CompanionPresence.CurrentDocumentName ?? "No document reported yet")</dd>
|
<span data-onboarding-build-progress></span>
|
||||||
</div>
|
</div>
|
||||||
</dl>
|
<div class="onboarding-manual-save-actions" data-onboarding-manual-save-actions hidden>
|
||||||
<section class="onboarding-scan-panel"
|
<button class="btn btn-primary btn-sm" type="button" data-onboarding-build-save-confirm disabled>
|
||||||
data-onboarding-scan-panel
|
I've saved the Word document
|
||||||
data-onboarding-auto-review
|
</button>
|
||||||
data-scan-status="@Model.ScanState.Status"
|
</div>
|
||||||
aria-label="Manuscript scan">
|
</div>
|
||||||
|
</section>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<section class="onboarding-companion-card onboarding-companion-card--compact @(Model.CompanionPresence.IsConnected ? "is-connected" : "is-offline")"
|
||||||
|
data-word-companion-presence
|
||||||
|
data-word-companion-status="@Model.CompanionPresence.Status"
|
||||||
|
aria-label="Word Companion connection">
|
||||||
|
<div class="onboarding-companion-mark" aria-hidden="true">
|
||||||
|
<span></span>
|
||||||
|
</div>
|
||||||
|
<div class="onboarding-companion-body">
|
||||||
|
<p class="eyebrow">Word Companion</p>
|
||||||
|
<h2 data-word-companion-waiting-status>@(Model.CompanionPresence.IsConnected ? "Word Companion Connected" : "Waiting for Word Companion...")</h2>
|
||||||
|
<p data-word-companion-connected-copy>
|
||||||
|
@CompanionScanMessage(Model.CompanionPresence, Model.ScanState)
|
||||||
|
</p>
|
||||||
|
<dl class="onboarding-companion-details">
|
||||||
|
<div>
|
||||||
|
<dt>Version</dt>
|
||||||
|
<dd data-word-companion-version>@(Model.CompanionPresence.CompanionVersion ?? "Waiting")</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Document</dt>
|
||||||
|
<dd data-word-companion-document>@(Model.CompanionPresence.CurrentDocumentName ?? "No document reported yet")</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
<section class="onboarding-scan-panel"
|
||||||
|
data-onboarding-scan-panel
|
||||||
|
data-onboarding-auto-review
|
||||||
|
data-scan-status="@Model.ScanState.Status"
|
||||||
|
aria-label="Manuscript scan">
|
||||||
<div class="onboarding-scan-status">
|
<div class="onboarding-scan-status">
|
||||||
<span data-onboarding-scan-message>@ScanMessage(Model.ScanState)</span>
|
<span data-onboarding-scan-message>@ScanMessage(Model.ScanState)</span>
|
||||||
<strong data-onboarding-scan-percent>@ScanPercent(Model.ScanState)</strong>
|
<strong data-onboarding-scan-percent>@ScanPercent(Model.ScanState)</strong>
|
||||||
@ -306,27 +366,10 @@
|
|||||||
<strong data-onboarding-scan-words>@Model.ScanState.TotalWordCount.ToString("N0")</strong>
|
<strong data-onboarding-scan-words>@Model.ScanState.TotalWordCount.ToString("N0")</strong>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@if (string.Equals(Model.ScanState.ReviewStatus, ManuscriptScanReviewStatuses.ReadyToImport, StringComparison.Ordinal))
|
</section>
|
||||||
{
|
</div>
|
||||||
<div class="onboarding-build-panel" data-onboarding-build-panel>
|
</section>
|
||||||
<div>
|
}
|
||||||
<p class="onboarding-scan-next">Ready to detect scenes.</p>
|
|
||||||
<p class="onboarding-build-message" data-onboarding-build-message>Keep Word open while markers are written. If Word cannot save automatically, PlotDirector will ask you to save and confirm.</p>
|
|
||||||
</div>
|
|
||||||
<strong data-onboarding-build-percent></strong>
|
|
||||||
<div class="onboarding-scan-progress onboarding-build-progress" aria-hidden="true">
|
|
||||||
<span data-onboarding-build-progress></span>
|
|
||||||
</div>
|
|
||||||
<div class="onboarding-manual-save-actions" data-onboarding-manual-save-actions hidden>
|
|
||||||
<button class="btn btn-primary btn-sm" type="button" data-onboarding-build-save-confirm disabled>
|
|
||||||
I've saved the Word document
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
}
|
}
|
||||||
<div class="onboarding-actions">
|
<div class="onboarding-actions">
|
||||||
@if (Model.IsMicrosoftWordPath && reviewedScanAvailable && Model.ScanState.PreviewID.HasValue)
|
@if (Model.IsMicrosoftWordPath && reviewedScanAvailable && Model.ScanState.PreviewID.HasValue)
|
||||||
|
|||||||
@ -105,14 +105,6 @@
|
|||||||
|
|
||||||
@if (!isCharacterStep)
|
@if (!isCharacterStep)
|
||||||
{
|
{
|
||||||
<label class="onboarding-inline-choice">
|
|
||||||
<input type="checkbox" name="GenerateSceneTitles" value="true" checked="@Model.GenerateSceneTitles" />
|
|
||||||
<input type="hidden" name="GenerateSceneTitles" value="false" />
|
|
||||||
<span>
|
|
||||||
<strong>Generate descriptive scene titles</strong>
|
|
||||||
<small>Titles are suggested during scene detection and can be edited later. Word markers stay unchanged.</small>
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
<div class="onboarding-review-tools" data-review-tools>
|
<div class="onboarding-review-tools" data-review-tools>
|
||||||
<button class="btn btn-sm btn-outline-secondary" type="button" data-bulk-target="chapter" data-bulk-state="true">Include all chapters</button>
|
<button class="btn btn-sm btn-outline-secondary" type="button" data-bulk-target="chapter" data-bulk-state="true">Include all chapters</button>
|
||||||
<button class="btn btn-sm btn-outline-secondary" type="button" data-bulk-target="chapter" data-bulk-state="false">Exclude all chapters</button>
|
<button class="btn btn-sm btn-outline-secondary" type="button" data-bulk-target="chapter" data-bulk-state="false">Exclude all chapters</button>
|
||||||
|
|||||||
@ -425,6 +425,25 @@
|
|||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.onboarding-detect-scenes {
|
||||||
|
display: grid;
|
||||||
|
gap: .85rem;
|
||||||
|
margin: 1rem 0 1.25rem;
|
||||||
|
padding: 1rem;
|
||||||
|
border: 1px solid rgba(31, 42, 68, .12);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: rgba(255, 255, 255, .72);
|
||||||
|
}
|
||||||
|
|
||||||
|
.onboarding-detect-scenes > p:not(.onboarding-compact-status) {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--bs-secondary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.onboarding-detect-scenes .onboarding-inline-choice {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.onboarding-build-panel {
|
.onboarding-build-panel {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(0, 1fr) auto;
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
|
|||||||
@ -349,6 +349,7 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
pendingBuildPreviewId = previewId;
|
pendingBuildPreviewId = previewId;
|
||||||
|
const generateSceneTitles = document.querySelector("[data-onboarding-generate-scene-titles]")?.checked !== false;
|
||||||
clearManualSaveRequired();
|
clearManualSaveRequired();
|
||||||
button.disabled = true;
|
button.disabled = true;
|
||||||
document.querySelectorAll("[data-onboarding-build-panel]").forEach((node) => {
|
document.querySelectorAll("[data-onboarding-build-panel]").forEach((node) => {
|
||||||
@ -364,7 +365,7 @@
|
|||||||
node.style.width = "0%";
|
node.style.width = "0%";
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
const result = await connection.invoke("StartOnboardingProjectBuild", previewId);
|
const result = await connection.invoke("StartOnboardingProjectBuild", previewId, generateSceneTitles);
|
||||||
pendingBuildPreviewId = field(result, "previewID", "PreviewID") || previewId;
|
pendingBuildPreviewId = field(result, "previewID", "PreviewID") || previewId;
|
||||||
document.querySelectorAll("[data-onboarding-build-message]").forEach((node) => {
|
document.querySelectorAll("[data-onboarding-build-message]").forEach((node) => {
|
||||||
node.textContent = "Updating Word document with scene markers. Keep Word open, then save the document when prompted.";
|
node.textContent = "Updating Word document with scene markers. Keep Word open, then save the document when prompted.";
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user