Phase 20AI – Story Intelligence Pipeline & Character Review
This commit is contained in:
parent
7a48a91f21
commit
a3a6c52287
@ -109,8 +109,32 @@ public sealed class OnboardingController(IOnboardingService onboarding, IOnboard
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
return model.HasActiveRuns
|
||||
? RedirectToAction(nameof(StoryIntelligenceProgress), new { batchId })
|
||||
if (model.HasActiveRuns)
|
||||
{
|
||||
return RedirectToAction(nameof(StoryIntelligenceProgress), new { batchId });
|
||||
}
|
||||
|
||||
return model.AllCommitted
|
||||
? RedirectToAction(nameof(StoryIntelligenceCharacters), new { batchId })
|
||||
: View(model);
|
||||
}
|
||||
|
||||
[HttpGet("story-intelligence/characters")]
|
||||
public async Task<IActionResult> StoryIntelligenceCharacters(Guid batchId)
|
||||
{
|
||||
var model = await storyIntelligence.GetProgressAsync(batchId);
|
||||
if (model is null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
if (model.HasActiveRuns)
|
||||
{
|
||||
return RedirectToAction(nameof(StoryIntelligenceProgress), new { batchId });
|
||||
}
|
||||
|
||||
return !model.AllCommitted
|
||||
? RedirectToAction(nameof(StoryIntelligenceReview), new { batchId })
|
||||
: View(model);
|
||||
}
|
||||
|
||||
@ -160,7 +184,7 @@ public sealed class OnboardingController(IOnboardingService onboarding, IOnboard
|
||||
|
||||
TempData[result.Success ? "OnboardingStoryIntelligenceMessage" : "OnboardingStoryIntelligenceError"] = result.Message;
|
||||
return result.Success && progress.AllCommitted
|
||||
? RedirectToAction(nameof(StoryIntelligenceComplete), new { batchId })
|
||||
? RedirectToAction(nameof(StoryIntelligenceCharacters), new { batchId })
|
||||
: RedirectToAction(nameof(StoryIntelligenceReview), new { batchId });
|
||||
}
|
||||
|
||||
@ -175,12 +199,32 @@ public sealed class OnboardingController(IOnboardingService onboarding, IOnboard
|
||||
}
|
||||
|
||||
TempData[result.Success ? "OnboardingStoryIntelligenceMessage" : "OnboardingStoryIntelligenceError"] = result.Message;
|
||||
return RedirectToAction(nameof(StoryIntelligenceReview), new { batchId = form.BatchID });
|
||||
return result.Success
|
||||
? RedirectToAction(nameof(StoryIntelligenceCharacterComplete), new { batchId = form.BatchID })
|
||||
: RedirectToAction(nameof(StoryIntelligenceCharacters), new { batchId = form.BatchID });
|
||||
}
|
||||
|
||||
[HttpGet("story-intelligence/characters/complete")]
|
||||
public async Task<IActionResult> StoryIntelligenceCharacterComplete(Guid batchId)
|
||||
{
|
||||
var model = await storyIntelligence.GetCharacterImportResultAsync(batchId);
|
||||
return model is null ? NotFound() : View(model);
|
||||
}
|
||||
|
||||
[HttpGet("story-intelligence/complete")]
|
||||
public async Task<IActionResult> StoryIntelligenceComplete(Guid batchId)
|
||||
{
|
||||
var progress = await storyIntelligence.GetProgressAsync(batchId);
|
||||
if (progress is null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
if (!progress.PipelineDashboard.CharacterStageComplete)
|
||||
{
|
||||
return RedirectToAction(nameof(StoryIntelligenceCharacters), new { batchId });
|
||||
}
|
||||
|
||||
var model = await storyIntelligence.GetCompletionAsync(batchId);
|
||||
return model is null ? NotFound() : View(model);
|
||||
}
|
||||
|
||||
@ -15,6 +15,7 @@ public interface IOnboardingStoryIntelligenceService
|
||||
Task<(StoryIntelligenceProgressViewModel? Progress, StoryIntelligenceImportCommitResult Result)> CommitAsync(Guid batchId, int runId);
|
||||
Task<(StoryIntelligenceProgressViewModel? Progress, StoryIntelligenceImportCommitResult Result)> CommitAllAsync(Guid batchId);
|
||||
Task<(StoryIntelligenceProgressViewModel? Progress, StoryIntelligenceImportCommitResult Result)> ImportCharactersAsync(Guid batchId, StoryIntelligenceCharacterImportForm form);
|
||||
Task<StoryIntelligenceCharacterImportResultViewModel?> GetCharacterImportResultAsync(Guid batchId);
|
||||
Task<StoryIntelligenceCompletionViewModel?> GetCompletionAsync(Guid batchId);
|
||||
}
|
||||
|
||||
@ -255,6 +256,7 @@ public sealed class OnboardingStoryIntelligenceService(
|
||||
});
|
||||
}
|
||||
|
||||
var characterReview = await characterImport.BuildReviewAsync(batch);
|
||||
return new StoryIntelligenceProgressViewModel
|
||||
{
|
||||
BatchID = batch.BatchID,
|
||||
@ -264,7 +266,15 @@ public sealed class OnboardingStoryIntelligenceService(
|
||||
ProjectName = batch.ProjectName,
|
||||
BookTitle = batch.BookTitle,
|
||||
Chapters = chapters,
|
||||
CharacterReview = await characterImport.BuildReviewAsync(batch)
|
||||
CharacterReview = characterReview,
|
||||
PipelineDashboard = new StoryIntelligencePipelineDashboardViewModel
|
||||
{
|
||||
ChaptersAnalysed = chapters.Count(chapter => chapter.IsRunComplete),
|
||||
ScenesImported = chapters.Sum(chapter => chapter.ScenesCreated),
|
||||
CharactersIdentified = characterReview.Candidates.Count + batch.CharacterDecisions.Count,
|
||||
CharactersCreatedOrLinked = batch.CharacterDecisions.Count(decision => decision.CreatedOrLinked),
|
||||
CharacterStageComplete = batch.CharacterStageComplete || characterReview.IsComplete
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@ -343,6 +353,27 @@ public sealed class OnboardingStoryIntelligenceService(
|
||||
return (await GetProgressAsync(batchId), result);
|
||||
}
|
||||
|
||||
public async Task<StoryIntelligenceCharacterImportResultViewModel?> GetCharacterImportResultAsync(Guid batchId)
|
||||
{
|
||||
var batch = await batchStore.GetAsync(RequireUserId(), batchId);
|
||||
if (batch is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new StoryIntelligenceCharacterImportResultViewModel
|
||||
{
|
||||
BatchID = batch.BatchID,
|
||||
ProjectID = batch.ProjectID,
|
||||
BookID = batch.BookID,
|
||||
CharactersCreated = batch.LastCharacterImportResult?.CharactersCreated ?? 0,
|
||||
CharactersLinked = batch.LastCharacterImportResult?.CharactersLinked ?? 0,
|
||||
CharactersIgnored = batch.LastCharacterImportResult?.CharactersIgnored ?? 0,
|
||||
PovLinksResolved = batch.LastCharacterImportResult?.PovLinksResolved ?? 0,
|
||||
ScenePeoplePanelsUpdated = batch.LastCharacterImportResult?.ScenePeoplePanelsUpdated ?? 0
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<StoryIntelligenceCompletionViewModel?> GetCompletionAsync(Guid batchId)
|
||||
{
|
||||
var progress = await GetProgressAsync(batchId);
|
||||
@ -360,7 +391,9 @@ public sealed class OnboardingStoryIntelligenceService(
|
||||
ProjectName = progress.ProjectName,
|
||||
BookTitle = progress.BookTitle,
|
||||
ChaptersCommitted = progress.CommittedChapterCount,
|
||||
ScenesCreated = progress.Chapters.Sum(chapter => chapter.ScenesCreated)
|
||||
ScenesCreated = progress.Chapters.Sum(chapter => chapter.ScenesCreated),
|
||||
CharactersCreatedOrLinked = progress.PipelineDashboard.CharactersCreatedOrLinked,
|
||||
CharacterStageComplete = progress.PipelineDashboard.CharacterStageComplete
|
||||
};
|
||||
}
|
||||
|
||||
@ -591,6 +624,9 @@ public sealed class OnboardingStoryIntelligenceBatch
|
||||
public string? BookTitle { get; init; }
|
||||
public DateTime CreatedUtc { get; init; } = DateTime.UtcNow;
|
||||
public IReadOnlyList<OnboardingStoryIntelligenceBatchItem> Items { get; init; } = [];
|
||||
public List<OnboardingStoryIntelligenceCharacterDecision> CharacterDecisions { get; } = [];
|
||||
public StoryIntelligenceCharacterImportBatchResult? LastCharacterImportResult { get; set; }
|
||||
public bool CharacterStageComplete { get; set; }
|
||||
}
|
||||
|
||||
public sealed class OnboardingStoryIntelligenceBatchItem
|
||||
@ -601,3 +637,21 @@ public sealed class OnboardingStoryIntelligenceBatchItem
|
||||
public int ChapterID { get; init; }
|
||||
public int RunID { get; init; }
|
||||
}
|
||||
|
||||
public sealed class OnboardingStoryIntelligenceCharacterDecision
|
||||
{
|
||||
public string Key { get; init; } = string.Empty;
|
||||
public string Action { get; init; } = string.Empty;
|
||||
public string? CharacterName { get; init; }
|
||||
public int? CharacterID { get; init; }
|
||||
public bool CreatedOrLinked { get; init; }
|
||||
}
|
||||
|
||||
public sealed class StoryIntelligenceCharacterImportBatchResult
|
||||
{
|
||||
public int CharactersCreated { get; init; }
|
||||
public int CharactersLinked { get; init; }
|
||||
public int CharactersIgnored { get; init; }
|
||||
public int PovLinksResolved { get; init; }
|
||||
public int ScenePeoplePanelsUpdated { get; init; }
|
||||
}
|
||||
|
||||
@ -33,12 +33,15 @@ public sealed class StoryIntelligenceCharacterImportService(
|
||||
public async Task<StoryIntelligenceCharacterReviewViewModel> BuildReviewAsync(OnboardingStoryIntelligenceBatch batch)
|
||||
{
|
||||
var data = await BuildCandidateDataAsync(batch);
|
||||
var decidedKeys = batch.CharacterDecisions.Select(decision => decision.Key).ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
var visibleCandidates = data.Candidates.Where(candidate => !decidedKeys.Contains(candidate.Key)).ToList();
|
||||
return new StoryIntelligenceCharacterReviewViewModel
|
||||
{
|
||||
HasCommittedScenes = data.HasCommittedScenes,
|
||||
CanImport = data.Candidates.Count > 0,
|
||||
CanImport = visibleCandidates.Count > 0,
|
||||
AlreadyLinkedCount = data.AlreadyLinkedCount,
|
||||
Candidates = data.Candidates.Select(candidate => new StoryIntelligenceCharacterReviewCandidateViewModel
|
||||
IsComplete = data.HasCommittedScenes && visibleCandidates.Count == 0,
|
||||
Candidates = visibleCandidates.Select(candidate => new StoryIntelligenceCharacterReviewCandidateViewModel
|
||||
{
|
||||
Key = candidate.Key,
|
||||
CharacterName = candidate.DisplayName,
|
||||
@ -62,11 +65,11 @@ public sealed class StoryIntelligenceCharacterImportService(
|
||||
}
|
||||
|
||||
var choices = form.Characters
|
||||
.Where(choice => choice.Include && !string.IsNullOrWhiteSpace(choice.Key))
|
||||
.Where(choice => !string.IsNullOrWhiteSpace(choice.Key))
|
||||
.ToDictionary(choice => choice.Key, choice => choice, StringComparer.OrdinalIgnoreCase);
|
||||
if (choices.Count == 0)
|
||||
{
|
||||
return new StoryIntelligenceImportCommitResult { Success = false, Message = "Choose at least one character to create or link." };
|
||||
return new StoryIntelligenceImportCommitResult { Success = false, Message = "Choose at least one character to create, link or ignore." };
|
||||
}
|
||||
|
||||
var existingIndex = await BuildCharacterIndexAsync(batch.ProjectID);
|
||||
@ -74,6 +77,8 @@ public sealed class StoryIntelligenceCharacterImportService(
|
||||
var roleTypes = lookupData.RoleTypes;
|
||||
var presenceTypes = lookupData.PresenceTypes;
|
||||
var created = 0;
|
||||
var linkedExisting = 0;
|
||||
var ignored = 0;
|
||||
var linkedAppearances = 0;
|
||||
var povLinks = 0;
|
||||
var resolvedCharacters = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
|
||||
@ -85,11 +90,22 @@ public sealed class StoryIntelligenceCharacterImportService(
|
||||
continue;
|
||||
}
|
||||
|
||||
if (string.Equals(choice.Action, StoryIntelligenceCharacterImportActions.Ignore, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
ignored++;
|
||||
AddDecision(batch, candidate.Key, StoryIntelligenceCharacterImportActions.Ignore, candidate.DisplayName, null, false);
|
||||
continue;
|
||||
}
|
||||
|
||||
var requestedName = Clean(choice.ImportName);
|
||||
var importName = string.IsNullOrWhiteSpace(requestedName) ? candidate.DisplayName : requestedName;
|
||||
var characterId = candidate.ExistingCharacterID
|
||||
var forceCreateSeparate = string.Equals(choice.Action, StoryIntelligenceCharacterImportActions.CreateSeparate, StringComparison.OrdinalIgnoreCase);
|
||||
var matchedExistingId = forceCreateSeparate
|
||||
? null
|
||||
: candidate.ExistingCharacterID
|
||||
?? existingIndex.Find(importName)?.CharacterID
|
||||
?? existingIndex.Find(candidate.DisplayName)?.CharacterID;
|
||||
var characterId = matchedExistingId;
|
||||
|
||||
if (!characterId.HasValue)
|
||||
{
|
||||
@ -102,6 +118,10 @@ public sealed class StoryIntelligenceCharacterImportService(
|
||||
});
|
||||
created++;
|
||||
}
|
||||
else
|
||||
{
|
||||
linkedExisting++;
|
||||
}
|
||||
|
||||
AddResolvedName(resolvedCharacters, candidate.DisplayName, characterId.Value);
|
||||
AddResolvedName(resolvedCharacters, importName, characterId.Value);
|
||||
@ -123,6 +143,8 @@ public sealed class StoryIntelligenceCharacterImportService(
|
||||
});
|
||||
linkedAppearances++;
|
||||
}
|
||||
|
||||
AddDecision(batch, candidate.Key, choice.Action, importName, characterId.Value, true);
|
||||
}
|
||||
|
||||
foreach (var scene in data.SceneAnalyses)
|
||||
@ -137,10 +159,22 @@ public sealed class StoryIntelligenceCharacterImportService(
|
||||
povLinks++;
|
||||
}
|
||||
|
||||
batch.CharacterStageComplete = true;
|
||||
batch.LastCharacterImportResult = new StoryIntelligenceCharacterImportBatchResult
|
||||
{
|
||||
CharactersCreated = created,
|
||||
CharactersLinked = linkedExisting,
|
||||
CharactersIgnored = ignored,
|
||||
PovLinksResolved = povLinks,
|
||||
ScenePeoplePanelsUpdated = linkedAppearances
|
||||
};
|
||||
|
||||
logger.LogInformation(
|
||||
"Imported Story Intelligence characters for batch {BatchID}. Created={Created} LinkedAppearances={LinkedAppearances} PovLinks={PovLinks}",
|
||||
"Imported Story Intelligence characters for batch {BatchID}. Created={Created} LinkedExisting={LinkedExisting} Ignored={Ignored} LinkedAppearances={LinkedAppearances} PovLinks={PovLinks}",
|
||||
batch.BatchID,
|
||||
created,
|
||||
linkedExisting,
|
||||
ignored,
|
||||
linkedAppearances,
|
||||
povLinks);
|
||||
|
||||
@ -148,10 +182,23 @@ public sealed class StoryIntelligenceCharacterImportService(
|
||||
{
|
||||
Success = true,
|
||||
ScenesCreated = linkedAppearances,
|
||||
Message = $"Characters imported. {created:N0} character(s) created, {linkedAppearances:N0} scene appearance(s) linked, {povLinks:N0} POV link(s) updated."
|
||||
Message = $"Characters imported. {created:N0} created, {linkedExisting:N0} linked, {ignored:N0} ignored, {linkedAppearances:N0} scene appearance(s) updated, {povLinks:N0} POV link(s) resolved."
|
||||
};
|
||||
}
|
||||
|
||||
private static void AddDecision(OnboardingStoryIntelligenceBatch batch, string key, string action, string? characterName, int? characterId, bool createdOrLinked)
|
||||
{
|
||||
batch.CharacterDecisions.RemoveAll(decision => string.Equals(decision.Key, key, StringComparison.OrdinalIgnoreCase));
|
||||
batch.CharacterDecisions.Add(new OnboardingStoryIntelligenceCharacterDecision
|
||||
{
|
||||
Key = key,
|
||||
Action = action,
|
||||
CharacterName = characterName,
|
||||
CharacterID = characterId,
|
||||
CreatedOrLinked = createdOrLinked
|
||||
});
|
||||
}
|
||||
|
||||
private async Task<CharacterCandidateData> BuildCandidateDataAsync(OnboardingStoryIntelligenceBatch batch)
|
||||
{
|
||||
var existingIndex = await BuildCharacterIndexAsync(batch.ProjectID);
|
||||
|
||||
@ -39,6 +39,11 @@ public sealed class OnboardingJourneyHeaderViewModel
|
||||
public string AriaLabel { get; init; } = "Manuscript import progress";
|
||||
}
|
||||
|
||||
public sealed class StoryIntelligencePipelineHeaderViewModel
|
||||
{
|
||||
public string CurrentStage { get; init; } = "Analyse Manuscript";
|
||||
}
|
||||
|
||||
public sealed class WritingJourneyOnboardingForm
|
||||
{
|
||||
[Required(ErrorMessage = "Choose the closest writing stage before continuing.")]
|
||||
@ -138,6 +143,7 @@ public sealed class StoryIntelligenceProgressViewModel
|
||||
public string? BookTitle { get; init; }
|
||||
public IReadOnlyList<StoryIntelligenceOnboardingChapterViewModel> Chapters { get; init; } = [];
|
||||
public StoryIntelligenceCharacterReviewViewModel CharacterReview { get; init; } = new();
|
||||
public StoryIntelligencePipelineDashboardViewModel PipelineDashboard { get; init; } = new();
|
||||
public int ChapterCount => Chapters.Count;
|
||||
public int CompletedChapterCount => Chapters.Count(chapter => chapter.IsRunComplete);
|
||||
public int CommittedChapterCount => Chapters.Count(chapter => chapter.HasCompletedCommit);
|
||||
@ -154,11 +160,21 @@ public sealed class StoryIntelligenceProgressViewModel
|
||||
public bool HasCharactersToReview => CharacterReview.Candidates.Count > 0;
|
||||
}
|
||||
|
||||
public sealed class StoryIntelligencePipelineDashboardViewModel
|
||||
{
|
||||
public int ChaptersAnalysed { get; init; }
|
||||
public int ScenesImported { get; init; }
|
||||
public int CharactersIdentified { get; init; }
|
||||
public int CharactersCreatedOrLinked { get; init; }
|
||||
public bool CharacterStageComplete { get; init; }
|
||||
}
|
||||
|
||||
public sealed class StoryIntelligenceCharacterReviewViewModel
|
||||
{
|
||||
public bool CanImport { get; init; }
|
||||
public bool HasCommittedScenes { get; init; }
|
||||
public int AlreadyLinkedCount { get; init; }
|
||||
public bool IsComplete { get; init; }
|
||||
public IReadOnlyList<StoryIntelligenceCharacterReviewCandidateViewModel> Candidates { get; init; } = [];
|
||||
}
|
||||
|
||||
@ -186,10 +202,17 @@ public sealed class StoryIntelligenceCharacterImportForm
|
||||
public sealed class StoryIntelligenceCharacterImportChoiceForm
|
||||
{
|
||||
public string Key { get; set; } = string.Empty;
|
||||
public bool Include { get; set; }
|
||||
public string Action { get; set; } = StoryIntelligenceCharacterImportActions.Approve;
|
||||
public string? ImportName { get; set; }
|
||||
}
|
||||
|
||||
public static class StoryIntelligenceCharacterImportActions
|
||||
{
|
||||
public const string Approve = "Approve";
|
||||
public const string Ignore = "Ignore";
|
||||
public const string CreateSeparate = "CreateSeparate";
|
||||
}
|
||||
|
||||
public sealed class StoryIntelligenceCompletionViewModel
|
||||
{
|
||||
public StoryIntelligenceJobProgress Job { get; init; } = new();
|
||||
@ -201,6 +224,20 @@ public sealed class StoryIntelligenceCompletionViewModel
|
||||
public string? BookTitle { get; init; }
|
||||
public int ChaptersCommitted { get; init; }
|
||||
public int ScenesCreated { get; init; }
|
||||
public int CharactersCreatedOrLinked { get; init; }
|
||||
public bool CharacterStageComplete { get; init; }
|
||||
}
|
||||
|
||||
public sealed class StoryIntelligenceCharacterImportResultViewModel
|
||||
{
|
||||
public Guid BatchID { get; init; }
|
||||
public int ProjectID { get; init; }
|
||||
public int BookID { get; init; }
|
||||
public int CharactersCreated { get; init; }
|
||||
public int CharactersLinked { get; init; }
|
||||
public int CharactersIgnored { get; init; }
|
||||
public int PovLinksResolved { get; init; }
|
||||
public int ScenePeoplePanelsUpdated { get; init; }
|
||||
}
|
||||
|
||||
public sealed class StoryIntelligenceOnboardingChapterViewModel
|
||||
|
||||
@ -5,9 +5,9 @@
|
||||
|
||||
<section class="onboarding-shell" aria-labelledby="story-intelligence-title">
|
||||
<div class="onboarding-panel">
|
||||
<partial name="_OnboardingJourneyHeader" model="@(new OnboardingJourneyHeaderViewModel { CurrentStep = 8 })" />
|
||||
<partial name="_StoryIntelligencePipelineHeader" model="@(new StoryIntelligencePipelineHeaderViewModel { CurrentStage = "Analyse Manuscript" })" />
|
||||
<div class="onboarding-copy">
|
||||
<p class="eyebrow">Step 8 of 9</p>
|
||||
<p class="eyebrow">Analyse manuscript</p>
|
||||
<h1 id="story-intelligence-title">Analyse Manuscript</h1>
|
||||
<p>PlotDirector will analyse approved chapters in the background and prepare scene suggestions for review.</p>
|
||||
</div>
|
||||
@ -27,7 +27,7 @@
|
||||
<h2>Analysis has already started</h2>
|
||||
<p>You can continue reviewing progress for the approved chapters.</p>
|
||||
<div class="onboarding-actions">
|
||||
<a class="btn btn-primary" asp-action="StoryIntelligenceReview" asp-route-batchId="@Model.ExistingBatchID">Review analysis</a>
|
||||
<a class="btn btn-primary" asp-action="StoryIntelligenceReview" asp-route-batchId="@Model.ExistingBatchID">Continue pipeline</a>
|
||||
<form asp-action="ScanManuscript" method="post">
|
||||
<button class="btn btn-outline-secondary" type="submit">Scan manuscript again</button>
|
||||
</form>
|
||||
@ -61,7 +61,7 @@
|
||||
|
||||
<section class="onboarding-form-section">
|
||||
<p>Nothing will be created yet. PlotDirector will prepare scene suggestions for review.</p>
|
||||
<p>For this step, only scenes can be created in PlotDirector. Characters, locations, assets and relationships stay as later suggestions.</p>
|
||||
<p>Scenes are reviewed first. Character review follows after scenes are created, with locations, assets, relationships and knowledge shown as upcoming Story Intelligence stages.</p>
|
||||
</section>
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(Model.Message))
|
||||
|
||||
@ -0,0 +1,53 @@
|
||||
@model StoryIntelligenceCharacterImportResultViewModel
|
||||
@{
|
||||
ViewData["Title"] = "Characters created";
|
||||
}
|
||||
|
||||
<section class="onboarding-shell" aria-labelledby="story-character-complete-title">
|
||||
<div class="onboarding-panel onboarding-review-panel">
|
||||
<partial name="_StoryIntelligencePipelineHeader" model="@(new StoryIntelligencePipelineHeaderViewModel { CurrentStage = "Create Characters" })" />
|
||||
<div class="onboarding-copy">
|
||||
<p class="eyebrow">Build Story Database</p>
|
||||
<div class="onboarding-success-heading">
|
||||
<span aria-hidden="true">✓</span>
|
||||
<h1 id="story-character-complete-title">Characters created</h1>
|
||||
</div>
|
||||
<p>PlotDirector updated your story database from the approved character decisions. The next Story Intelligence stages are ready to slot in as they are implemented.</p>
|
||||
</div>
|
||||
|
||||
<div class="onboarding-scan-counts onboarding-review-counts">
|
||||
<div>
|
||||
<span>Characters created</span>
|
||||
<strong>@Model.CharactersCreated.ToString("N0")</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Characters linked</span>
|
||||
<strong>@Model.CharactersLinked.ToString("N0")</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Characters ignored</span>
|
||||
<strong>@Model.CharactersIgnored.ToString("N0")</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>POV links resolved</span>
|
||||
<strong>@Model.PovLinksResolved.ToString("N0")</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Scene People panels updated</span>
|
||||
<strong>@Model.ScenePeoplePanelsUpdated.ToString("N0")</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="story-future-stage-grid">
|
||||
<article class="story-future-stage"><strong>Review Locations</strong><span>Coming Soon</span></article>
|
||||
<article class="story-future-stage"><strong>Review Assets</strong><span>Coming Soon</span></article>
|
||||
<article class="story-future-stage"><strong>Review Relationships</strong><span>Coming Soon</span></article>
|
||||
<article class="story-future-stage"><strong>Review Knowledge</strong><span>Coming Soon</span></article>
|
||||
</div>
|
||||
|
||||
<div class="onboarding-actions">
|
||||
<a class="btn btn-outline-secondary" asp-action="StoryIntelligenceCharacters" asp-route-batchId="@Model.BatchID">Back to characters</a>
|
||||
<a class="btn btn-primary" asp-action="StoryIntelligenceComplete" asp-route-batchId="@Model.BatchID">Continue</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
159
PlotLine/Views/Onboarding/StoryIntelligenceCharacters.cshtml
Normal file
159
PlotLine/Views/Onboarding/StoryIntelligenceCharacters.cshtml
Normal file
@ -0,0 +1,159 @@
|
||||
@model StoryIntelligenceProgressViewModel
|
||||
@{
|
||||
ViewData["Title"] = "Review characters";
|
||||
}
|
||||
|
||||
<section class="onboarding-shell" aria-labelledby="story-character-title">
|
||||
<div class="onboarding-panel onboarding-review-panel story-review-page">
|
||||
<partial name="_StoryIntelligencePipelineHeader" model="@(new StoryIntelligencePipelineHeaderViewModel { CurrentStage = "Review Characters" })" />
|
||||
<div class="onboarding-copy">
|
||||
<p class="eyebrow">Review Story Intelligence</p>
|
||||
<h1 id="story-character-title">Review Characters</h1>
|
||||
<p>Approve the characters PlotDirector should create or link from your imported scenes. You stay in control of every character record.</p>
|
||||
</div>
|
||||
|
||||
@if (TempData["OnboardingStoryIntelligenceError"] is string error)
|
||||
{
|
||||
<div class="alert alert-danger">@error</div>
|
||||
}
|
||||
@if (TempData["OnboardingStoryIntelligenceMessage"] is string message)
|
||||
{
|
||||
<div class="alert alert-success">@message</div>
|
||||
}
|
||||
|
||||
<partial name="_StoryIntelligencePipelineSummary" model="Model.PipelineDashboard" />
|
||||
|
||||
<section class="story-review-chapter-list" aria-label="Character review">
|
||||
@if (Model.CharacterReview.Candidates.Count == 0)
|
||||
{
|
||||
<div class="story-review-note">
|
||||
<strong>No character decisions are waiting.</strong>
|
||||
<p class="mb-0">Characters are already up to date, or there were no character suggestions ready to import.</p>
|
||||
</div>
|
||||
<div class="story-future-stage-grid">
|
||||
@FutureStage("Review Locations")
|
||||
@FutureStage("Review Assets")
|
||||
@FutureStage("Review Relationships")
|
||||
@FutureStage("Review Knowledge")
|
||||
</div>
|
||||
<div class="onboarding-actions">
|
||||
<a class="btn btn-outline-secondary" asp-action="StoryIntelligenceReview" asp-route-batchId="@Model.BatchID">Back to scenes</a>
|
||||
<a class="btn btn-primary" asp-action="StoryIntelligenceComplete" asp-route-batchId="@Model.BatchID">Continue</a>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="story-review-card-actions">
|
||||
<button class="btn btn-outline-primary btn-sm" type="button" data-character-bulk="approve-all">Approve all</button>
|
||||
<button class="btn btn-outline-secondary btn-sm" type="button" data-character-bulk="approve-selected">Approve selected</button>
|
||||
<button class="btn btn-outline-secondary btn-sm" type="button" data-character-bulk="ignore-selected">Ignore selected</button>
|
||||
<button class="btn btn-outline-secondary btn-sm" type="button" data-character-bulk="expand">Expand all</button>
|
||||
<button class="btn btn-outline-secondary btn-sm" type="button" data-character-bulk="collapse">Collapse all</button>
|
||||
</div>
|
||||
|
||||
<form asp-action="ImportStoryIntelligenceCharacters" method="post" data-character-review-form>
|
||||
<input type="hidden" name="BatchID" value="@Model.BatchID" />
|
||||
<div class="story-character-card-grid">
|
||||
@for (var i = 0; i < Model.CharacterReview.Candidates.Count; i++)
|
||||
{
|
||||
var candidate = Model.CharacterReview.Candidates[i];
|
||||
<details class="story-character-card" open data-character-card>
|
||||
<summary>
|
||||
<input type="checkbox" checked data-character-selected aria-label="Select @candidate.CharacterName" />
|
||||
<span>
|
||||
<strong>@candidate.CharacterName</strong>
|
||||
<small>@candidate.AppearsInScenes.ToString("N0") scene@(candidate.AppearsInScenes == 1 ? string.Empty : "s")</small>
|
||||
</span>
|
||||
<em>@candidate.Confidence</em>
|
||||
</summary>
|
||||
|
||||
<input type="hidden" name="Characters[@i].Key" value="@candidate.Key" />
|
||||
|
||||
<div class="story-character-card__body">
|
||||
<dl>
|
||||
<div><dt>First appearance</dt><dd>@Display(candidate.ExampleFirstAppearance)</dd></div>
|
||||
<div><dt>Possible aliases</dt><dd>@(candidate.PossibleAliases.Count == 0 ? "None detected" : string.Join(", ", candidate.PossibleAliases))</dd></div>
|
||||
<div><dt>Existing match</dt><dd>@(candidate.ExistingCharacterName ?? "None")</dd></div>
|
||||
</dl>
|
||||
|
||||
@if (candidate.IsExistingMatch)
|
||||
{
|
||||
<div class="story-review-note">
|
||||
<strong>Possible existing character</strong>
|
||||
<p>@candidate.CharacterName may already be @candidate.ExistingCharacterName. Choose whether to link them or create a separate character.</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
<fieldset class="story-character-actions">
|
||||
<legend>Decision</legend>
|
||||
<label>
|
||||
<input type="radio" name="Characters[@i].Action" value="@StoryIntelligenceCharacterImportActions.Approve" checked data-character-action />
|
||||
@(candidate.IsExistingMatch ? "Link to existing" : "Create new character")
|
||||
</label>
|
||||
@if (candidate.IsExistingMatch)
|
||||
{
|
||||
<label>
|
||||
<input type="radio" name="Characters[@i].Action" value="@StoryIntelligenceCharacterImportActions.CreateSeparate" data-character-action />
|
||||
Create separate character
|
||||
</label>
|
||||
}
|
||||
<label>
|
||||
<input type="radio" name="Characters[@i].Action" value="@StoryIntelligenceCharacterImportActions.Ignore" data-character-action />
|
||||
Ignore
|
||||
</label>
|
||||
</fieldset>
|
||||
|
||||
<label class="form-label" for="character-import-name-@i">Import name</label>
|
||||
<input id="character-import-name-@i" class="form-control" name="Characters[@i].ImportName" value="@candidate.ImportName" />
|
||||
</div>
|
||||
</details>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="onboarding-actions">
|
||||
<a class="btn btn-outline-secondary" asp-action="StoryIntelligenceReview" asp-route-batchId="@Model.BatchID">Back to scenes</a>
|
||||
<button class="btn btn-primary" type="submit">Create characters</button>
|
||||
</div>
|
||||
</form>
|
||||
}
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@section Scripts {
|
||||
<script>
|
||||
(() => {
|
||||
const form = document.querySelector("[data-character-review-form]");
|
||||
if (!form) return;
|
||||
|
||||
const cards = () => Array.from(form.querySelectorAll("[data-character-card]"));
|
||||
const setAction = (card, action) => {
|
||||
const input = card.querySelector(`[data-character-action][value="${action}"]`);
|
||||
if (input) input.checked = true;
|
||||
};
|
||||
document.querySelectorAll("[data-character-bulk]").forEach(button => {
|
||||
button.addEventListener("click", () => {
|
||||
const action = button.getAttribute("data-character-bulk");
|
||||
const selectedCards = cards().filter(card => card.querySelector("[data-character-selected]")?.checked);
|
||||
if (action === "approve-all") cards().forEach(card => setAction(card, "@StoryIntelligenceCharacterImportActions.Approve"));
|
||||
if (action === "approve-selected") selectedCards.forEach(card => setAction(card, "@StoryIntelligenceCharacterImportActions.Approve"));
|
||||
if (action === "ignore-selected") selectedCards.forEach(card => setAction(card, "@StoryIntelligenceCharacterImportActions.Ignore"));
|
||||
if (action === "expand") cards().forEach(card => card.open = true);
|
||||
if (action === "collapse") cards().forEach(card => card.open = false);
|
||||
});
|
||||
});
|
||||
form.addEventListener("submit", () => {
|
||||
cards()
|
||||
.filter(card => !card.querySelector("[data-character-selected]")?.checked)
|
||||
.forEach(card => setAction(card, "@StoryIntelligenceCharacterImportActions.Ignore"));
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
}
|
||||
|
||||
@functions {
|
||||
private static string Display(string? value) => string.IsNullOrWhiteSpace(value) ? "Not detected" : value;
|
||||
|
||||
private static Microsoft.AspNetCore.Html.IHtmlContent FutureStage(string label)
|
||||
=> new Microsoft.AspNetCore.Html.HtmlString($"<article class=\"story-future-stage\"><strong>{System.Net.WebUtility.HtmlEncode(label)}</strong><span>Coming Soon</span></article>");
|
||||
}
|
||||
@ -5,14 +5,14 @@
|
||||
|
||||
<section class="onboarding-shell" aria-labelledby="story-complete-title">
|
||||
<div class="onboarding-panel onboarding-review-panel">
|
||||
<partial name="_OnboardingJourneyHeader" model="@(new OnboardingJourneyHeaderViewModel { CurrentStep = 9 })" />
|
||||
<partial name="_StoryIntelligencePipelineHeader" model="@(new StoryIntelligencePipelineHeaderViewModel { CurrentStage = "Complete" })" />
|
||||
<div class="onboarding-copy">
|
||||
<p class="eyebrow">Step 9 of 9</p>
|
||||
<p class="eyebrow">Story Intelligence pipeline</p>
|
||||
<div class="onboarding-success-heading">
|
||||
<span aria-hidden="true">✓</span>
|
||||
<h1 id="story-complete-title">Manuscript import complete</h1>
|
||||
<h1 id="story-complete-title">Story Intelligence complete</h1>
|
||||
</div>
|
||||
<p>PlotDirector created scenes from the approved manuscript chapters. Characters, locations, assets and relationships were left untouched.</p>
|
||||
<p>PlotDirector completed every implemented Story Intelligence stage for this manuscript. Future stages will continue from this same pipeline as they become available.</p>
|
||||
</div>
|
||||
|
||||
<div class="onboarding-scan-counts onboarding-review-counts">
|
||||
@ -25,8 +25,8 @@
|
||||
<strong>@Model.ScenesCreated</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Notes needing review</span>
|
||||
<strong>None imported</strong>
|
||||
<span>Characters created / linked</span>
|
||||
<strong>@Model.CharactersCreatedOrLinked.ToString("N0")</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Book</span>
|
||||
@ -34,6 +34,13 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="story-future-stage-grid">
|
||||
<article class="story-future-stage"><strong>Review Locations</strong><span>Coming Soon</span></article>
|
||||
<article class="story-future-stage"><strong>Review Assets</strong><span>Coming Soon</span></article>
|
||||
<article class="story-future-stage"><strong>Review Relationships</strong><span>Coming Soon</span></article>
|
||||
<article class="story-future-stage"><strong>Review Knowledge</strong><span>Coming Soon</span></article>
|
||||
</div>
|
||||
|
||||
<div class="onboarding-actions">
|
||||
<a class="btn btn-primary" asp-controller="Writer" asp-action="Index" asp-route-projectId="@Model.ProjectID">View scenes</a>
|
||||
<a class="btn btn-outline-primary" asp-controller="Books" asp-action="Details" asp-route-id="@Model.BookID">View book</a>
|
||||
|
||||
@ -13,9 +13,9 @@
|
||||
data-story-intelligence-run-ids="@runIds"
|
||||
data-story-max-progress-percent="@progressPercent"
|
||||
data-story-intelligence-has-active-runs="@Model.HasActiveRuns.ToString().ToLowerInvariant()">
|
||||
<partial name="_OnboardingJourneyHeader" model="@(new OnboardingJourneyHeaderViewModel { CurrentStep = 8 })" />
|
||||
<partial name="_StoryIntelligencePipelineHeader" model="@(new StoryIntelligencePipelineHeaderViewModel { CurrentStage = "Analyse Manuscript" })" />
|
||||
<div class="onboarding-copy story-live-hero">
|
||||
<p class="eyebrow">Step 8 of 9</p>
|
||||
<p class="eyebrow">Analyse manuscript</p>
|
||||
<h1 id="story-progress-title">Reading your manuscript</h1>
|
||||
<p>PlotDirector is analysing your chapters and preparing scenes for review. You can safely leave this page and return later.</p>
|
||||
</div>
|
||||
@ -142,7 +142,7 @@
|
||||
<a class="btn btn-primary"
|
||||
data-story-review-link
|
||||
asp-action="StoryIntelligenceReview"
|
||||
asp-route-batchId="@Model.BatchID">Review analysis</a>
|
||||
asp-route-batchId="@Model.BatchID">Review scenes</a>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
@using System.Security.Claims
|
||||
@inject IConfiguration Configuration
|
||||
@{
|
||||
ViewData["Title"] = "Review analysis";
|
||||
ViewData["Title"] = "Review scenes";
|
||||
var adminEmails = Configuration.GetSection("Admin:AllowedEmails").Get<string[]>() ?? [];
|
||||
var userEmail = User.FindFirstValue(ClaimTypes.Email);
|
||||
var isAdmin = !string.IsNullOrWhiteSpace(userEmail)
|
||||
@ -13,11 +13,11 @@
|
||||
|
||||
<section class="onboarding-shell" aria-labelledby="story-review-title">
|
||||
<div class="onboarding-panel onboarding-review-panel story-review-page">
|
||||
<partial name="_OnboardingJourneyHeader" model="@(new OnboardingJourneyHeaderViewModel { CurrentStep = 9 })" />
|
||||
<partial name="_StoryIntelligencePipelineHeader" model="@(new StoryIntelligencePipelineHeaderViewModel { CurrentStage = "Review Scenes" })" />
|
||||
<div class="onboarding-copy">
|
||||
<p class="eyebrow">Step 9 of 9</p>
|
||||
<h1 id="story-review-title">Review Story Intelligence Results</h1>
|
||||
<p>PlotDirector has finished reading your manuscript. Review the detected scenes first, then approve any characters you want to add or link.</p>
|
||||
<p class="eyebrow">Review Story Intelligence</p>
|
||||
<h1 id="story-review-title">Review Scenes</h1>
|
||||
<p>PlotDirector has finished reading your manuscript. Review the detected scenes before creating them in your story database.</p>
|
||||
</div>
|
||||
|
||||
@if (TempData["OnboardingStoryIntelligenceError"] is string error)
|
||||
@ -178,56 +178,6 @@
|
||||
Scene creation uses PlotDirector import safeguards. Duplicate imports are blocked, and failures roll back safely.
|
||||
</section>
|
||||
|
||||
@if (Model.CharacterReview.HasCommittedScenes)
|
||||
{
|
||||
<section class="story-review-chapter-list" aria-label="Character review">
|
||||
<h2>Review Characters</h2>
|
||||
@if (Model.CharacterReview.Candidates.Count == 0)
|
||||
{
|
||||
<p class="text-muted">No new character suggestions are waiting. Existing scene character links are already up to date where PlotDirector could match them.</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<p>Approve the characters PlotDirector should create or link from the imported scenes. You can rename new characters before import.</p>
|
||||
<form asp-action="ImportStoryIntelligenceCharacters" method="post">
|
||||
<input type="hidden" name="BatchID" value="@Model.BatchID" />
|
||||
<div class="story-scene-preview-list">
|
||||
@for (var i = 0; i < Model.CharacterReview.Candidates.Count; i++)
|
||||
{
|
||||
var candidate = Model.CharacterReview.Candidates[i];
|
||||
<article class="story-scene-preview">
|
||||
<input type="hidden" name="Characters[@i].Key" value="@candidate.Key" />
|
||||
<div>
|
||||
<label>
|
||||
<input type="checkbox" name="Characters[@i].Include" value="true" checked />
|
||||
<strong>@candidate.CharacterName</strong>
|
||||
</label>
|
||||
<span class="story-review-status story-review-status--ready">@candidate.ActionLabel</span>
|
||||
</div>
|
||||
<dl>
|
||||
<div><dt>Appears in</dt><dd>@candidate.AppearsInScenes.ToString("N0") scene@(candidate.AppearsInScenes == 1 ? string.Empty : "s")</dd></div>
|
||||
<div><dt>Confidence</dt><dd>@candidate.Confidence</dd></div>
|
||||
<div><dt>Match</dt><dd>@(candidate.ExistingCharacterName ?? "New character")</dd></div>
|
||||
</dl>
|
||||
@if (candidate.PossibleAliases.Count > 0)
|
||||
{
|
||||
<p>Aliases: @string.Join(", ", candidate.PossibleAliases)</p>
|
||||
}
|
||||
@if (!string.IsNullOrWhiteSpace(candidate.ExampleFirstAppearance))
|
||||
{
|
||||
<p>@candidate.ExampleFirstAppearance</p>
|
||||
}
|
||||
<label class="form-label" for="character-import-name-@i">Import name</label>
|
||||
<input id="character-import-name-@i" class="form-control" name="Characters[@i].ImportName" value="@candidate.ImportName" readonly="@candidate.IsExistingMatch" />
|
||||
</article>
|
||||
}
|
||||
</div>
|
||||
<button class="btn btn-primary mt-3" type="submit">Import approved characters</button>
|
||||
</form>
|
||||
}
|
||||
</section>
|
||||
}
|
||||
|
||||
<div class="onboarding-actions">
|
||||
@if (Model.BatchID.HasValue)
|
||||
{
|
||||
@ -241,7 +191,7 @@
|
||||
}
|
||||
else if (Model.AllCommitted)
|
||||
{
|
||||
<a class="btn btn-primary" asp-action="StoryIntelligenceComplete" asp-route-batchId="@Model.BatchID">Continue</a>
|
||||
<a class="btn btn-primary" asp-action="StoryIntelligenceCharacters" asp-route-batchId="@Model.BatchID">Review characters</a>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
@ -0,0 +1,50 @@
|
||||
@model StoryIntelligencePipelineHeaderViewModel
|
||||
@{
|
||||
var phases = new[]
|
||||
{
|
||||
new { Name = "Phase 1", Label = "Prepare manuscript", Stages = new[] { "Connect Word Companion", "Scan Manuscript", "Review Chapters" } },
|
||||
new { Name = "Phase 2", Label = "Analyse manuscript", Stages = new[] { "Analyse Manuscript" } },
|
||||
new { Name = "Phase 3", Label = "Review Story Intelligence", Stages = new[] { "Review Scenes", "Review Characters", "Review Locations", "Review Assets", "Review Relationships", "Review Knowledge" } },
|
||||
new { Name = "Phase 4", Label = "Build Story Database", Stages = new[] { "Create Scenes", "Create Characters", "Complete" } }
|
||||
};
|
||||
var flatStages = phases.SelectMany(phase => phase.Stages).ToList();
|
||||
var currentIndex = Math.Max(0, flatStages.FindIndex(stage => string.Equals(stage, Model.CurrentStage, StringComparison.OrdinalIgnoreCase)));
|
||||
var percent = flatStages.Count <= 1 ? 100 : Math.Clamp((currentIndex * 100) / (flatStages.Count - 1), 0, 100);
|
||||
}
|
||||
|
||||
<div class="onboarding-progress story-pipeline-progress" aria-label="Story Intelligence pipeline progress">
|
||||
<span>@Model.CurrentStage</span>
|
||||
<div class="onboarding-progress-track">
|
||||
<span style="width:@percent%"></span>
|
||||
</div>
|
||||
</div>
|
||||
<ol class="onboarding-stepper story-pipeline-stepper" aria-label="Story Intelligence pipeline">
|
||||
@foreach (var phase in phases)
|
||||
{
|
||||
var firstStageIndex = flatStages.FindIndex(stage => phase.Stages.Contains(stage));
|
||||
var lastStageIndex = firstStageIndex + phase.Stages.Length - 1;
|
||||
var phaseClass = currentIndex > lastStageIndex
|
||||
? "is-complete"
|
||||
: currentIndex >= firstStageIndex && currentIndex <= lastStageIndex
|
||||
? "is-current"
|
||||
: "is-upcoming";
|
||||
<li class="@phaseClass">
|
||||
<span>@phase.Name.Replace("Phase ", string.Empty)</span>
|
||||
<strong>@phase.Label</strong>
|
||||
<small>@StageSummary(phase.Stages, currentIndex, firstStageIndex)</small>
|
||||
</li>
|
||||
}
|
||||
</ol>
|
||||
|
||||
@functions {
|
||||
private static string StageSummary(IReadOnlyList<string> stages, int currentIndex, int firstStageIndex)
|
||||
{
|
||||
var localIndex = currentIndex - firstStageIndex;
|
||||
if (localIndex >= 0 && localIndex < stages.Count)
|
||||
{
|
||||
return stages[localIndex];
|
||||
}
|
||||
|
||||
return stages.Count == 1 ? stages[0] : $"{stages.Count} stages";
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,36 @@
|
||||
@model StoryIntelligencePipelineDashboardViewModel
|
||||
|
||||
<section class="story-review-summary story-pipeline-summary" aria-label="Story Intelligence progress summary">
|
||||
<div>
|
||||
<span>Chapters analysed</span>
|
||||
<strong>✓ @Model.ChaptersAnalysed.ToString("N0")</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Scenes imported</span>
|
||||
<strong>✓ @Model.ScenesImported.ToString("N0")</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Characters identified</span>
|
||||
<strong>@Model.CharactersIdentified.ToString("N0")</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Characters created / linked</span>
|
||||
<strong>@(Model.CharacterStageComplete ? $"Done: {Model.CharactersCreatedOrLinked:N0}" : "In review")</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Locations</span>
|
||||
<strong>Coming next</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Assets</span>
|
||||
<strong>Coming next</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Relationships</span>
|
||||
<strong>Coming next</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Knowledge</span>
|
||||
<strong>Coming next</strong>
|
||||
</div>
|
||||
</section>
|
||||
@ -863,6 +863,122 @@ summary.story-review-chapter-heading {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: .65rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.story-pipeline-stepper small {
|
||||
display: block;
|
||||
color: var(--bs-secondary-color);
|
||||
font-size: .72rem;
|
||||
font-weight: 700;
|
||||
margin-top: .15rem;
|
||||
}
|
||||
|
||||
.story-character-card-grid,
|
||||
.story-future-stage-grid {
|
||||
display: grid;
|
||||
gap: .75rem;
|
||||
}
|
||||
|
||||
.story-character-card {
|
||||
border: 1px solid rgba(31, 42, 68, .1);
|
||||
border-radius: 8px;
|
||||
background: rgba(255, 255, 255, .64);
|
||||
padding: .8rem;
|
||||
}
|
||||
|
||||
.story-character-card summary {
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr) auto;
|
||||
gap: .65rem;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.story-character-card summary span {
|
||||
display: grid;
|
||||
gap: .1rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.story-character-card summary small,
|
||||
.story-character-card summary em {
|
||||
color: var(--bs-secondary-color);
|
||||
font-size: .82rem;
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.story-character-card__body {
|
||||
display: grid;
|
||||
gap: .75rem;
|
||||
padding-top: .85rem;
|
||||
}
|
||||
|
||||
.story-character-card__body dl {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: .65rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.story-character-card__body dt {
|
||||
color: var(--bs-secondary-color);
|
||||
font-size: .78rem;
|
||||
font-weight: 800;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.story-character-card__body dd {
|
||||
margin: .15rem 0 0;
|
||||
}
|
||||
|
||||
.story-character-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: .75rem;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.story-character-actions legend {
|
||||
width: 100%;
|
||||
color: var(--bs-secondary-color);
|
||||
font-size: .78rem;
|
||||
font-weight: 800;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.story-character-actions label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: .35rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.story-future-stage-grid {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.story-future-stage {
|
||||
border: 1px dashed rgba(31, 42, 68, .22);
|
||||
border-radius: 8px;
|
||||
padding: .85rem;
|
||||
background: rgba(255, 255, 255, .45);
|
||||
}
|
||||
|
||||
.story-future-stage strong,
|
||||
.story-future-stage span {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.story-future-stage span {
|
||||
color: var(--bs-secondary-color);
|
||||
font-size: .84rem;
|
||||
font-weight: 700;
|
||||
margin-top: .2rem;
|
||||
}
|
||||
|
||||
.story-scene-preview-list {
|
||||
@ -1283,6 +1399,8 @@ summary.story-review-chapter-heading {
|
||||
.story-live-stats,
|
||||
.story-review-summary,
|
||||
.story-review-scene-panel,
|
||||
.story-character-card__body dl,
|
||||
.story-future-stage-grid,
|
||||
.story-scene-preview dl,
|
||||
.story-live-current,
|
||||
.story-live-feed-grid,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user