Phase 20AJ – Story Intelligence Pipeline Completion and Resume Support
This commit is contained in:
parent
a3a6c52287
commit
438478c7fe
@ -62,6 +62,24 @@ public sealed class OnboardingController(IOnboardingService onboarding, IOnboard
|
||||
return View(model);
|
||||
}
|
||||
|
||||
[HttpGet("story-intelligence/book/{bookId:int}/continue")]
|
||||
public async Task<IActionResult> ContinueStoryIntelligence(int bookId)
|
||||
{
|
||||
var target = await storyIntelligence.ResumeBookAsync(bookId);
|
||||
if (target is null)
|
||||
{
|
||||
TempData["ArchiveError"] = "Story Intelligence could not find saved analysis for that book.";
|
||||
return RedirectToAction("Details", "Books", new { id = bookId });
|
||||
}
|
||||
|
||||
return target.Route switch
|
||||
{
|
||||
StoryIntelligenceResumeRoutes.Complete => RedirectToAction(nameof(StoryIntelligenceComplete), new { batchId = target.BatchID }),
|
||||
StoryIntelligenceResumeRoutes.Characters => RedirectToAction(nameof(StoryIntelligenceCharacters), new { batchId = target.BatchID }),
|
||||
_ => RedirectToAction(nameof(StoryIntelligenceReview), new { batchId = target.BatchID })
|
||||
};
|
||||
}
|
||||
|
||||
[HttpPost("story-intelligence/start")]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> StartStoryIntelligence()
|
||||
|
||||
74
PlotLine/Data/StoryIntelligencePipelineRepository.cs
Normal file
74
PlotLine/Data/StoryIntelligencePipelineRepository.cs
Normal file
@ -0,0 +1,74 @@
|
||||
using System.Data;
|
||||
using Dapper;
|
||||
using PlotLine.Models;
|
||||
|
||||
namespace PlotLine.Data;
|
||||
|
||||
public interface IStoryIntelligencePipelineRepository
|
||||
{
|
||||
Task<StoryIntelligenceBookPipelineState?> GetByBookAsync(int bookId);
|
||||
Task<StoryIntelligenceBookPipelineState?> GetByBookForUserAsync(int bookId, int userId);
|
||||
Task<IReadOnlyList<StoryIntelligenceBookPipelineState>> ListForUserAsync(int userId);
|
||||
Task<StoryIntelligenceBookPipelineState> UpsertAsync(StoryIntelligenceBookPipelineSaveRequest request);
|
||||
Task<IReadOnlyList<StoryIntelligenceCommittedRunSummary>> ListCommittedRunsByBookAsync(int bookId, int userId);
|
||||
}
|
||||
|
||||
public sealed class StoryIntelligencePipelineRepository(ISqlConnectionFactory connectionFactory) : IStoryIntelligencePipelineRepository
|
||||
{
|
||||
public async Task<StoryIntelligenceBookPipelineState?> GetByBookAsync(int bookId)
|
||||
{
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
return await connection.QuerySingleOrDefaultAsync<StoryIntelligenceBookPipelineState>(
|
||||
"dbo.StoryIntelligenceBookPipeline_GetByBook",
|
||||
new { BookID = bookId },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
}
|
||||
|
||||
public async Task<StoryIntelligenceBookPipelineState?> GetByBookForUserAsync(int bookId, int userId)
|
||||
{
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
return await connection.QuerySingleOrDefaultAsync<StoryIntelligenceBookPipelineState>(
|
||||
"dbo.StoryIntelligenceBookPipeline_GetByBookForUser",
|
||||
new { BookID = bookId, UserID = userId },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<StoryIntelligenceBookPipelineState>> ListForUserAsync(int userId)
|
||||
{
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
var rows = await connection.QueryAsync<StoryIntelligenceBookPipelineState>(
|
||||
"dbo.StoryIntelligenceBookPipeline_ListForUser",
|
||||
new { UserID = userId },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
return rows.ToList();
|
||||
}
|
||||
|
||||
public async Task<StoryIntelligenceBookPipelineState> UpsertAsync(StoryIntelligenceBookPipelineSaveRequest request)
|
||||
{
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
return await connection.QuerySingleAsync<StoryIntelligenceBookPipelineState>(
|
||||
"dbo.StoryIntelligenceBookPipeline_Upsert",
|
||||
new
|
||||
{
|
||||
request.ProjectID,
|
||||
request.BookID,
|
||||
request.CurrentStage,
|
||||
request.LastCompletedStage,
|
||||
request.CurrentReviewStage,
|
||||
request.Status,
|
||||
request.CompletedUtc,
|
||||
request.LastRunID
|
||||
},
|
||||
commandType: CommandType.StoredProcedure);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<StoryIntelligenceCommittedRunSummary>> ListCommittedRunsByBookAsync(int bookId, int userId)
|
||||
{
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
var rows = await connection.QueryAsync<StoryIntelligenceCommittedRunSummary>(
|
||||
"dbo.StoryIntelligenceBookPipeline_ListCommittedRunsByBook",
|
||||
new { BookID = bookId, UserID = userId },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
return rows.ToList();
|
||||
}
|
||||
}
|
||||
@ -21,6 +21,71 @@ public static class StoryIntelligenceFailureStages
|
||||
public const string DomainImport = "DomainImport";
|
||||
}
|
||||
|
||||
public static class StoryIntelligencePipelineStages
|
||||
{
|
||||
public const string Chapters = "Chapters";
|
||||
public const string SceneReview = "SceneReview";
|
||||
public const string SceneImport = "SceneImport";
|
||||
public const string CharacterReview = "CharacterReview";
|
||||
public const string CharacterImport = "CharacterImport";
|
||||
public const string Complete = "Complete";
|
||||
}
|
||||
|
||||
public static class StoryIntelligencePipelineStatuses
|
||||
{
|
||||
public const string InProgress = "InProgress";
|
||||
public const string NeedsReview = "NeedsReview";
|
||||
public const string Complete = "Complete";
|
||||
}
|
||||
|
||||
public sealed class StoryIntelligenceBookPipelineState
|
||||
{
|
||||
public int StoryIntelligenceBookPipelineID { get; init; }
|
||||
public int ProjectID { get; init; }
|
||||
public string ProjectName { get; init; } = string.Empty;
|
||||
public int BookID { get; init; }
|
||||
public string BookTitle { get; init; } = string.Empty;
|
||||
public string? BookSubtitle { get; init; }
|
||||
public string BookDisplayTitle => BookTitleFormatter.DisplayTitle(BookTitle, BookSubtitle);
|
||||
public string CurrentStage { get; init; } = StoryIntelligencePipelineStages.SceneReview;
|
||||
public string? LastCompletedStage { get; init; }
|
||||
public string? CurrentReviewStage { get; init; }
|
||||
public string Status { get; init; } = StoryIntelligencePipelineStatuses.NeedsReview;
|
||||
public DateTime? CompletedUtc { get; init; }
|
||||
public int? LastRunID { get; init; }
|
||||
public DateTime CreatedUtc { get; init; }
|
||||
public DateTime UpdatedUtc { get; init; }
|
||||
|
||||
public bool IsComplete => string.Equals(Status, StoryIntelligencePipelineStatuses.Complete, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public sealed class StoryIntelligenceBookPipelineSaveRequest
|
||||
{
|
||||
public int ProjectID { get; init; }
|
||||
public int BookID { get; init; }
|
||||
public string CurrentStage { get; init; } = StoryIntelligencePipelineStages.SceneReview;
|
||||
public string? LastCompletedStage { get; init; }
|
||||
public string? CurrentReviewStage { get; init; }
|
||||
public string Status { get; init; } = StoryIntelligencePipelineStatuses.NeedsReview;
|
||||
public DateTime? CompletedUtc { get; init; }
|
||||
public int? LastRunID { get; init; }
|
||||
}
|
||||
|
||||
public sealed class StoryIntelligenceCommittedRunSummary
|
||||
{
|
||||
public int StoryIntelligenceRunID { get; init; }
|
||||
public int UserID { get; init; }
|
||||
public int ProjectID { get; init; }
|
||||
public string ProjectName { get; init; } = string.Empty;
|
||||
public int BookID { get; init; }
|
||||
public string BookTitle { get; init; } = string.Empty;
|
||||
public string? BookSubtitle { get; init; }
|
||||
public int ChapterID { get; init; }
|
||||
public decimal ChapterNumber { get; init; }
|
||||
public string ChapterTitle { get; init; } = string.Empty;
|
||||
public string? SourceLabel { get; init; }
|
||||
}
|
||||
|
||||
public sealed class StoryIntelligenceRunSaveRequest
|
||||
{
|
||||
public int UserID { get; init; }
|
||||
|
||||
@ -143,6 +143,7 @@ public class Program
|
||||
builder.Services.AddScoped<IStoryIntelligenceRepository, StoryIntelligenceRepository>();
|
||||
builder.Services.AddScoped<IStoryIntelligenceResultRepository, StoryIntelligenceResultRepository>();
|
||||
builder.Services.AddScoped<IStoryIntelligenceSourceRepository, StoryIntelligenceSourceRepository>();
|
||||
builder.Services.AddScoped<IStoryIntelligencePipelineRepository, StoryIntelligencePipelineRepository>();
|
||||
builder.Services.AddScoped<IProjectAccessRepository, ProjectAccessRepository>();
|
||||
builder.Services.AddScoped<IProjectCollaborationRepository, ProjectCollaborationRepository>();
|
||||
builder.Services.AddScoped<IProjectActivityRepository, ProjectActivityRepository>();
|
||||
@ -207,6 +208,7 @@ public class Program
|
||||
builder.Services.AddScoped<IStoryIntelligenceFullChapterTestService, StoryIntelligenceFullChapterTestService>();
|
||||
builder.Services.AddScoped<IStoryIntelligenceImportCommitService, StoryIntelligenceImportCommitService>();
|
||||
builder.Services.AddScoped<IStoryIntelligenceCharacterImportService, StoryIntelligenceCharacterImportService>();
|
||||
builder.Services.AddScoped<IStoryIntelligencePipelineStateService, StoryIntelligencePipelineStateService>();
|
||||
builder.Services.AddScoped<IStoryIntelligenceProgressNotifier, StoryIntelligenceProgressNotifier>();
|
||||
builder.Services.AddScoped<ISceneImportResolver, SceneImportResolver>();
|
||||
builder.Services.AddScoped<IPersistedStoryIntelligenceRunner, PersistedStoryIntelligenceRunner>();
|
||||
|
||||
@ -1139,6 +1139,7 @@ public sealed class BookService(
|
||||
IBookCoverService bookCovers,
|
||||
ISubscriptionService subscriptions,
|
||||
IProjectActivityService activity,
|
||||
IStoryIntelligencePipelineStateService storyIntelligencePipeline,
|
||||
ICurrentUserService currentUser) : IBookService
|
||||
{
|
||||
public async Task<BookDetailViewModel?> GetDetailAsync(int bookId)
|
||||
@ -1162,7 +1163,10 @@ public sealed class BookService(
|
||||
ManuscriptDocument = currentUser.UserId is int userId
|
||||
? await manuscriptDocuments.GetByBookAsync(bookId, userId)
|
||||
: null,
|
||||
Chapters = await chapters.ListByBookAsync(bookId)
|
||||
Chapters = await chapters.ListByBookAsync(bookId),
|
||||
StoryIntelligencePipeline = currentUser.UserId is int pipelineUserId
|
||||
? await storyIntelligencePipeline.GetForBookAsync(bookId, pipelineUserId)
|
||||
: null
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -17,6 +17,7 @@ public interface IOnboardingStoryIntelligenceService
|
||||
Task<(StoryIntelligenceProgressViewModel? Progress, StoryIntelligenceImportCommitResult Result)> ImportCharactersAsync(Guid batchId, StoryIntelligenceCharacterImportForm form);
|
||||
Task<StoryIntelligenceCharacterImportResultViewModel?> GetCharacterImportResultAsync(Guid batchId);
|
||||
Task<StoryIntelligenceCompletionViewModel?> GetCompletionAsync(Guid batchId);
|
||||
Task<OnboardingStoryIntelligenceResumeTarget?> ResumeBookAsync(int bookId);
|
||||
}
|
||||
|
||||
public sealed class OnboardingStoryIntelligenceService(
|
||||
@ -26,6 +27,7 @@ public sealed class OnboardingStoryIntelligenceService(
|
||||
IStoryIntelligenceResultRepository runs,
|
||||
IStoryIntelligenceImportCommitService commits,
|
||||
IStoryIntelligenceCharacterImportService characterImport,
|
||||
IStoryIntelligencePipelineStateService pipelineState,
|
||||
IStoryIntelligenceClient client,
|
||||
IOnboardingStoryIntelligenceBatchStore batchStore,
|
||||
IStoryIntelligenceProgressNotifier notifier,
|
||||
@ -397,6 +399,64 @@ public sealed class OnboardingStoryIntelligenceService(
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<OnboardingStoryIntelligenceResumeTarget?> ResumeBookAsync(int bookId)
|
||||
{
|
||||
var userId = RequireUserId();
|
||||
var state = await pipelineState.GetForBookAsync(bookId, userId);
|
||||
if (state is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var committedRuns = await pipelineState.ListCommittedRunsByBookAsync(bookId, userId);
|
||||
if (committedRuns.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var batch = new OnboardingStoryIntelligenceBatch
|
||||
{
|
||||
UserID = userId,
|
||||
OnboardingID = 0,
|
||||
PreviewID = Guid.Empty,
|
||||
ProjectID = state.ProjectID,
|
||||
BookID = state.BookID,
|
||||
ProjectName = state.ProjectName,
|
||||
BookTitle = state.BookDisplayTitle,
|
||||
Items = committedRuns
|
||||
.OrderBy(run => run.ChapterNumber)
|
||||
.ThenBy(run => run.StoryIntelligenceRunID)
|
||||
.Select(run => new OnboardingStoryIntelligenceBatchItem
|
||||
{
|
||||
TemporaryChapterKey = $"book-{state.BookID}-chapter-{run.ChapterID}",
|
||||
ChapterNumber = Convert.ToInt32(run.ChapterNumber),
|
||||
ChapterTitle = run.ChapterTitle,
|
||||
ChapterID = run.ChapterID,
|
||||
RunID = run.StoryIntelligenceRunID
|
||||
})
|
||||
.ToList()
|
||||
};
|
||||
|
||||
if (state.IsComplete)
|
||||
{
|
||||
batch.CharacterStageComplete = true;
|
||||
}
|
||||
|
||||
await batchStore.SaveAsync(batch);
|
||||
|
||||
var route = state.CurrentStage switch
|
||||
{
|
||||
StoryIntelligencePipelineStages.Complete => StoryIntelligenceResumeRoutes.Complete,
|
||||
StoryIntelligencePipelineStages.CharacterReview => StoryIntelligenceResumeRoutes.Characters,
|
||||
StoryIntelligencePipelineStages.CharacterImport => StoryIntelligenceResumeRoutes.Characters,
|
||||
StoryIntelligencePipelineStages.SceneReview => StoryIntelligenceResumeRoutes.SceneReview,
|
||||
StoryIntelligencePipelineStages.SceneImport => StoryIntelligenceResumeRoutes.SceneReview,
|
||||
_ => StoryIntelligenceResumeRoutes.SceneReview
|
||||
};
|
||||
|
||||
return new OnboardingStoryIntelligenceResumeTarget(batch.BatchID, route);
|
||||
}
|
||||
|
||||
private async Task<(UserOnboardingState State, ManuscriptScanPreview Preview, ManuscriptScanReviewDecision Review, OnboardingManuscriptBuildResult? Build, OnboardingWizardViewModel Wizard)?> GetContextAsync(int userId)
|
||||
{
|
||||
var state = await onboardingRepository.GetAsync(userId);
|
||||
@ -655,3 +715,12 @@ public sealed class StoryIntelligenceCharacterImportBatchResult
|
||||
public int PovLinksResolved { get; init; }
|
||||
public int ScenePeoplePanelsUpdated { get; init; }
|
||||
}
|
||||
|
||||
public static class StoryIntelligenceResumeRoutes
|
||||
{
|
||||
public const string SceneReview = "SceneReview";
|
||||
public const string Characters = "Characters";
|
||||
public const string Complete = "Complete";
|
||||
}
|
||||
|
||||
public sealed record OnboardingStoryIntelligenceResumeTarget(Guid BatchID, string Route);
|
||||
|
||||
@ -15,6 +15,7 @@ public sealed class StoryIntelligenceCharacterImportService(
|
||||
IStoryIntelligenceResultRepository runs,
|
||||
ICharacterRepository characters,
|
||||
ISceneRepository scenes,
|
||||
IStoryIntelligencePipelineStateService pipelineState,
|
||||
ILogger<StoryIntelligenceCharacterImportService> logger) : IStoryIntelligenceCharacterImportService
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
@ -168,6 +169,7 @@ public sealed class StoryIntelligenceCharacterImportService(
|
||||
PovLinksResolved = povLinks,
|
||||
ScenePeoplePanelsUpdated = linkedAppearances
|
||||
};
|
||||
await pipelineState.RecordCharacterImportAsync(batch.ProjectID, batch.BookID);
|
||||
|
||||
logger.LogInformation(
|
||||
"Imported Story Intelligence characters for batch {BatchID}. Created={Created} LinkedExisting={LinkedExisting} Ignored={Ignored} LinkedAppearances={LinkedAppearances} PovLinks={PovLinks}",
|
||||
|
||||
@ -20,6 +20,7 @@ public sealed class StoryIntelligenceImportCommitService(
|
||||
ISceneMetricTypeRepository metricTypes,
|
||||
IWriterWorkspaceRepository writerWorkspace,
|
||||
ICharacterRepository characters,
|
||||
IStoryIntelligencePipelineStateService pipelineState,
|
||||
ILogger<StoryIntelligenceImportCommitService> logger) : IStoryIntelligenceImportCommitService
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
@ -97,6 +98,11 @@ public sealed class StoryIntelligenceImportCommitService(
|
||||
result.ScenesCreated,
|
||||
result.MetricsCreated);
|
||||
|
||||
if (result.Success && prepared.Run.ProjectID.HasValue && prepared.Run.BookID.HasValue)
|
||||
{
|
||||
await pipelineState.RecordSceneImportAsync(prepared.Run.ProjectID.Value, prepared.Run.BookID.Value, prepared.Run.StoryIntelligenceRunID);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
94
PlotLine/Services/StoryIntelligencePipelineStateService.cs
Normal file
94
PlotLine/Services/StoryIntelligencePipelineStateService.cs
Normal file
@ -0,0 +1,94 @@
|
||||
using PlotLine.Data;
|
||||
using PlotLine.Models;
|
||||
|
||||
namespace PlotLine.Services;
|
||||
|
||||
public interface IStoryIntelligencePipelineStateService
|
||||
{
|
||||
Task<StoryIntelligenceBookPipelineState?> GetForBookAsync(int bookId, int userId);
|
||||
Task<IReadOnlyList<StoryIntelligenceBookPipelineState>> ListForUserAsync(int userId);
|
||||
Task<StoryIntelligenceBookPipelineState?> EnsureForBookAsync(int bookId, int userId);
|
||||
Task RecordSceneImportAsync(int projectId, int bookId, int runId);
|
||||
Task RecordCharacterImportAsync(int projectId, int bookId);
|
||||
Task<IReadOnlyList<StoryIntelligenceCommittedRunSummary>> ListCommittedRunsByBookAsync(int bookId, int userId);
|
||||
}
|
||||
|
||||
public sealed class StoryIntelligencePipelineStateService(
|
||||
IStoryIntelligencePipelineRepository pipelines,
|
||||
ILogger<StoryIntelligencePipelineStateService> logger) : IStoryIntelligencePipelineStateService
|
||||
{
|
||||
public async Task<StoryIntelligenceBookPipelineState?> GetForBookAsync(int bookId, int userId)
|
||||
=> await pipelines.GetByBookForUserAsync(bookId, userId)
|
||||
?? await EnsureForBookAsync(bookId, userId);
|
||||
|
||||
public Task<IReadOnlyList<StoryIntelligenceBookPipelineState>> ListForUserAsync(int userId)
|
||||
=> pipelines.ListForUserAsync(userId);
|
||||
|
||||
public async Task<StoryIntelligenceBookPipelineState?> EnsureForBookAsync(int bookId, int userId)
|
||||
{
|
||||
var existing = await pipelines.GetByBookForUserAsync(bookId, userId);
|
||||
if (existing is not null)
|
||||
{
|
||||
return existing;
|
||||
}
|
||||
|
||||
var committedRuns = await pipelines.ListCommittedRunsByBookAsync(bookId, userId);
|
||||
if (committedRuns.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var latestRun = committedRuns.OrderByDescending(run => run.StoryIntelligenceRunID).First();
|
||||
var created = await pipelines.UpsertAsync(new StoryIntelligenceBookPipelineSaveRequest
|
||||
{
|
||||
ProjectID = latestRun.ProjectID,
|
||||
BookID = latestRun.BookID,
|
||||
CurrentStage = StoryIntelligencePipelineStages.CharacterReview,
|
||||
LastCompletedStage = StoryIntelligencePipelineStages.SceneImport,
|
||||
CurrentReviewStage = StoryIntelligencePipelineStages.CharacterReview,
|
||||
Status = StoryIntelligencePipelineStatuses.NeedsReview,
|
||||
CompletedUtc = null,
|
||||
LastRunID = latestRun.StoryIntelligenceRunID
|
||||
});
|
||||
|
||||
logger.LogInformation(
|
||||
"Created Story Intelligence pipeline state for existing imported book {BookID}. ProjectID={ProjectID} CurrentStage={CurrentStage}",
|
||||
bookId,
|
||||
created.ProjectID,
|
||||
created.CurrentStage);
|
||||
return created;
|
||||
}
|
||||
|
||||
public async Task RecordSceneImportAsync(int projectId, int bookId, int runId)
|
||||
{
|
||||
await pipelines.UpsertAsync(new StoryIntelligenceBookPipelineSaveRequest
|
||||
{
|
||||
ProjectID = projectId,
|
||||
BookID = bookId,
|
||||
CurrentStage = StoryIntelligencePipelineStages.CharacterReview,
|
||||
LastCompletedStage = StoryIntelligencePipelineStages.SceneImport,
|
||||
CurrentReviewStage = StoryIntelligencePipelineStages.CharacterReview,
|
||||
Status = StoryIntelligencePipelineStatuses.NeedsReview,
|
||||
CompletedUtc = null,
|
||||
LastRunID = runId
|
||||
});
|
||||
}
|
||||
|
||||
public async Task RecordCharacterImportAsync(int projectId, int bookId)
|
||||
{
|
||||
await pipelines.UpsertAsync(new StoryIntelligenceBookPipelineSaveRequest
|
||||
{
|
||||
ProjectID = projectId,
|
||||
BookID = bookId,
|
||||
CurrentStage = StoryIntelligencePipelineStages.Complete,
|
||||
LastCompletedStage = StoryIntelligencePipelineStages.CharacterImport,
|
||||
CurrentReviewStage = null,
|
||||
Status = StoryIntelligencePipelineStatuses.Complete,
|
||||
CompletedUtc = DateTime.UtcNow,
|
||||
LastRunID = null
|
||||
});
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<StoryIntelligenceCommittedRunSummary>> ListCommittedRunsByBookAsync(int bookId, int userId)
|
||||
=> pipelines.ListCommittedRunsByBookAsync(bookId, userId);
|
||||
}
|
||||
@ -69,6 +69,7 @@ public sealed class StubStoryIntelligenceProvider : IStoryIntelligenceProvider
|
||||
public sealed class StoryIntelligenceService(
|
||||
IStoryIntelligenceRepository repository,
|
||||
IStoryIntelligenceResultRepository persistedRuns,
|
||||
IStoryIntelligencePipelineStateService pipelineState,
|
||||
IOnboardingStoryIntelligenceBatchStore onboardingBatches,
|
||||
IOnboardingRepository onboarding,
|
||||
IProjectRepository projects,
|
||||
@ -148,6 +149,9 @@ public sealed class StoryIntelligenceService(
|
||||
return new StoryIntelligenceDashboardViewModel();
|
||||
}
|
||||
|
||||
var pipelineBooks = (await pipelineState.ListForUserAsync(currentUser.UserId.Value))
|
||||
.Select(ToPipelineBookStatus)
|
||||
.ToList();
|
||||
var activeBatch = await onboardingBatches.GetLatestActiveForUserAsync(currentUser.UserId.Value);
|
||||
if (activeBatch is not null)
|
||||
{
|
||||
@ -178,11 +182,27 @@ public sealed class StoryIntelligenceService(
|
||||
ActiveImportProgressLabel = $"Chapter {currentChapter:N0} of {activeBatch.Items.Count:N0}",
|
||||
Title = "Story Intelligence is reading",
|
||||
Description = "PlotDirector is analysing your manuscript in the background.",
|
||||
ButtonText = "View progress"
|
||||
ButtonText = "View progress",
|
||||
PipelineBooks = pipelineBooks
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
var nextPipeline = pipelineBooks.FirstOrDefault(item => !item.IsComplete) ?? pipelineBooks.FirstOrDefault();
|
||||
if (nextPipeline is not null)
|
||||
{
|
||||
return new StoryIntelligenceDashboardViewModel
|
||||
{
|
||||
ShouldShow = true,
|
||||
Title = nextPipeline.IsComplete ? "Story Intelligence Complete" : "Continue Story Intelligence",
|
||||
Description = nextPipeline.IsComplete
|
||||
? $"{nextPipeline.BookTitle} has completed the currently available Story Intelligence stages."
|
||||
: $"{nextPipeline.BookTitle} is waiting at the next Story Intelligence review stage.",
|
||||
ButtonText = nextPipeline.IsComplete ? "View summary" : "Continue Story Intelligence",
|
||||
PipelineBooks = pipelineBooks
|
||||
};
|
||||
}
|
||||
|
||||
var state = await onboarding.GetAsync(currentUser.UserId.Value);
|
||||
return state?.ProjectID.HasValue == true && state.BookID.HasValue
|
||||
? new StoryIntelligenceDashboardViewModel
|
||||
@ -190,7 +210,8 @@ public sealed class StoryIntelligenceService(
|
||||
ShouldShow = true,
|
||||
Title = "Import your manuscript",
|
||||
Description = "Scan your manuscript with the Word Companion, review the chapters, then let PlotDirector prepare scene suggestions.",
|
||||
ButtonText = "Continue manuscript import"
|
||||
ButtonText = "Continue manuscript import",
|
||||
PipelineBooks = pipelineBooks
|
||||
}
|
||||
: new StoryIntelligenceDashboardViewModel();
|
||||
}
|
||||
@ -229,6 +250,43 @@ public sealed class StoryIntelligenceService(
|
||||
private static string BuildBookTitle(Book? book)
|
||||
=> book is null ? "Selected book" : $"Book {book.BookNumber}: {book.BookDisplayTitle}";
|
||||
|
||||
private static StoryIntelligencePipelineBookStatusViewModel ToPipelineBookStatus(StoryIntelligenceBookPipelineState state)
|
||||
=> new()
|
||||
{
|
||||
BookID = state.BookID,
|
||||
BookTitle = state.BookDisplayTitle,
|
||||
StatusLabel = PipelineStatusLabel(state),
|
||||
Description = PipelineDescription(state),
|
||||
IsComplete = state.IsComplete
|
||||
};
|
||||
|
||||
private static string PipelineStatusLabel(StoryIntelligenceBookPipelineState state)
|
||||
{
|
||||
if (state.IsComplete)
|
||||
{
|
||||
return "Story Intelligence Complete";
|
||||
}
|
||||
|
||||
return state.CurrentReviewStage switch
|
||||
{
|
||||
StoryIntelligencePipelineStages.CharacterReview => "Waiting for Character Review",
|
||||
StoryIntelligencePipelineStages.SceneReview => "Needs Scene Review",
|
||||
_ => string.Equals(state.Status, StoryIntelligencePipelineStatuses.InProgress, StringComparison.OrdinalIgnoreCase)
|
||||
? "Story Intelligence In Progress"
|
||||
: "Story Intelligence Needs Review"
|
||||
};
|
||||
}
|
||||
|
||||
private static string PipelineDescription(StoryIntelligenceBookPipelineState state)
|
||||
=> state.IsComplete
|
||||
? "All currently available Story Intelligence stages are complete."
|
||||
: state.CurrentReviewStage switch
|
||||
{
|
||||
StoryIntelligencePipelineStages.CharacterReview => "Review detected characters and decide what to create or link.",
|
||||
StoryIntelligencePipelineStages.SceneReview => "Review prepared scenes before creating them.",
|
||||
_ => "Continue from the next Story Intelligence stage."
|
||||
};
|
||||
|
||||
private static string FormatElapsed(TimeSpan elapsed)
|
||||
=> elapsed.TotalMinutes < 1
|
||||
? $"{Math.Max(0, (int)elapsed.TotalSeconds)} sec"
|
||||
|
||||
208
PlotLine/Sql/127_Phase20AJ_StoryIntelligencePipelineState.sql
Normal file
208
PlotLine/Sql/127_Phase20AJ_StoryIntelligencePipelineState.sql
Normal file
@ -0,0 +1,208 @@
|
||||
SET ANSI_NULLS ON;
|
||||
GO
|
||||
SET QUOTED_IDENTIFIER ON;
|
||||
GO
|
||||
|
||||
IF OBJECT_ID(N'dbo.StoryIntelligenceBookPipelines', N'U') IS NULL
|
||||
BEGIN
|
||||
CREATE TABLE dbo.StoryIntelligenceBookPipelines
|
||||
(
|
||||
StoryIntelligenceBookPipelineID int IDENTITY(1,1) NOT NULL CONSTRAINT PK_StoryIntelligenceBookPipelines PRIMARY KEY,
|
||||
ProjectID int NOT NULL,
|
||||
BookID int NOT NULL,
|
||||
CurrentStage nvarchar(80) NOT NULL,
|
||||
LastCompletedStage nvarchar(80) NULL,
|
||||
CurrentReviewStage nvarchar(80) NULL,
|
||||
Status nvarchar(40) NOT NULL,
|
||||
CompletedUtc datetime2 NULL,
|
||||
LastRunID int NULL,
|
||||
CreatedUtc datetime2 NOT NULL CONSTRAINT DF_StoryIntelligenceBookPipelines_CreatedUtc DEFAULT SYSUTCDATETIME(),
|
||||
UpdatedUtc datetime2 NOT NULL CONSTRAINT DF_StoryIntelligenceBookPipelines_UpdatedUtc DEFAULT SYSUTCDATETIME(),
|
||||
CONSTRAINT FK_StoryIntelligenceBookPipelines_Projects FOREIGN KEY (ProjectID) REFERENCES dbo.Projects(ProjectID),
|
||||
CONSTRAINT FK_StoryIntelligenceBookPipelines_Books FOREIGN KEY (BookID) REFERENCES dbo.Books(BookID),
|
||||
CONSTRAINT FK_StoryIntelligenceBookPipelines_LastRun FOREIGN KEY (LastRunID) REFERENCES dbo.StoryIntelligenceRuns(StoryIntelligenceRunID),
|
||||
CONSTRAINT CK_StoryIntelligenceBookPipelines_Status CHECK (Status IN (N'InProgress', N'NeedsReview', N'Complete')),
|
||||
CONSTRAINT CK_StoryIntelligenceBookPipelines_CurrentStage CHECK (CurrentStage IN (N'Chapters', N'SceneReview', N'SceneImport', N'CharacterReview', N'CharacterImport', N'Complete'))
|
||||
);
|
||||
END;
|
||||
GO
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'UX_StoryIntelligenceBookPipelines_Book' AND object_id = OBJECT_ID(N'dbo.StoryIntelligenceBookPipelines'))
|
||||
CREATE UNIQUE INDEX UX_StoryIntelligenceBookPipelines_Book ON dbo.StoryIntelligenceBookPipelines(BookID);
|
||||
GO
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_StoryIntelligenceBookPipelines_Project_Status' AND object_id = OBJECT_ID(N'dbo.StoryIntelligenceBookPipelines'))
|
||||
CREATE INDEX IX_StoryIntelligenceBookPipelines_Project_Status ON dbo.StoryIntelligenceBookPipelines(ProjectID, Status, UpdatedUtc DESC);
|
||||
GO
|
||||
|
||||
;WITH LatestCommittedBookRun AS
|
||||
(
|
||||
SELECT CAST(r.ProjectID AS int) AS ProjectID,
|
||||
CAST(r.BookID AS int) AS BookID,
|
||||
r.StoryIntelligenceRunID,
|
||||
ROW_NUMBER() OVER (PARTITION BY r.BookID ORDER BY r.StoryIntelligenceRunID DESC) AS RowNumber
|
||||
FROM dbo.StoryIntelligenceRuns r
|
||||
INNER JOIN dbo.StoryIntelligenceImportCommits commitRows ON commitRows.StoryIntelligenceRunID = r.StoryIntelligenceRunID
|
||||
AND commitRows.Status = N'Completed'
|
||||
INNER JOIN dbo.Books b ON b.BookID = r.BookID
|
||||
INNER JOIN dbo.Projects p ON p.ProjectID = r.ProjectID
|
||||
WHERE r.ProjectID IS NOT NULL
|
||||
AND r.BookID IS NOT NULL
|
||||
AND b.IsArchived = 0
|
||||
AND p.IsArchived = 0
|
||||
)
|
||||
INSERT dbo.StoryIntelligenceBookPipelines
|
||||
(ProjectID, BookID, CurrentStage, LastCompletedStage, CurrentReviewStage, Status, CompletedUtc, LastRunID)
|
||||
SELECT source.ProjectID,
|
||||
source.BookID,
|
||||
N'CharacterReview',
|
||||
N'SceneImport',
|
||||
N'CharacterReview',
|
||||
N'NeedsReview',
|
||||
NULL,
|
||||
source.StoryIntelligenceRunID
|
||||
FROM LatestCommittedBookRun source
|
||||
WHERE source.RowNumber = 1
|
||||
AND NOT EXISTS
|
||||
(
|
||||
SELECT 1
|
||||
FROM dbo.StoryIntelligenceBookPipelines existing
|
||||
WHERE existing.BookID = source.BookID
|
||||
);
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.StoryIntelligenceBookPipeline_GetByBook
|
||||
@BookID int
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
SELECT p.StoryIntelligenceBookPipelineID, p.ProjectID, project.ProjectName,
|
||||
p.BookID, book.BookTitle, book.Subtitle AS BookSubtitle,
|
||||
p.CurrentStage, p.LastCompletedStage, p.CurrentReviewStage, p.Status,
|
||||
p.CompletedUtc, p.LastRunID, p.CreatedUtc, p.UpdatedUtc
|
||||
FROM dbo.StoryIntelligenceBookPipelines p
|
||||
INNER JOIN dbo.Projects project ON project.ProjectID = p.ProjectID
|
||||
INNER JOIN dbo.Books book ON book.BookID = p.BookID
|
||||
WHERE p.BookID = @BookID;
|
||||
END;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.StoryIntelligenceBookPipeline_Upsert
|
||||
@ProjectID int,
|
||||
@BookID int,
|
||||
@CurrentStage nvarchar(80),
|
||||
@LastCompletedStage nvarchar(80) = NULL,
|
||||
@CurrentReviewStage nvarchar(80) = NULL,
|
||||
@Status nvarchar(40),
|
||||
@CompletedUtc datetime2 = NULL,
|
||||
@LastRunID int = NULL
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
IF EXISTS (SELECT 1 FROM dbo.StoryIntelligenceBookPipelines WHERE BookID = @BookID)
|
||||
BEGIN
|
||||
UPDATE dbo.StoryIntelligenceBookPipelines
|
||||
SET ProjectID = @ProjectID,
|
||||
CurrentStage = @CurrentStage,
|
||||
LastCompletedStage = @LastCompletedStage,
|
||||
CurrentReviewStage = @CurrentReviewStage,
|
||||
Status = @Status,
|
||||
CompletedUtc = @CompletedUtc,
|
||||
LastRunID = COALESCE(@LastRunID, LastRunID),
|
||||
UpdatedUtc = SYSUTCDATETIME()
|
||||
WHERE BookID = @BookID;
|
||||
END
|
||||
ELSE
|
||||
BEGIN
|
||||
INSERT dbo.StoryIntelligenceBookPipelines
|
||||
(ProjectID, BookID, CurrentStage, LastCompletedStage, CurrentReviewStage, Status, CompletedUtc, LastRunID)
|
||||
VALUES
|
||||
(@ProjectID, @BookID, @CurrentStage, @LastCompletedStage, @CurrentReviewStage, @Status, @CompletedUtc, @LastRunID);
|
||||
END;
|
||||
|
||||
EXEC dbo.StoryIntelligenceBookPipeline_GetByBook @BookID = @BookID;
|
||||
END;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.StoryIntelligenceBookPipeline_GetByBookForUser
|
||||
@BookID int,
|
||||
@UserID int
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
SELECT p.StoryIntelligenceBookPipelineID, p.ProjectID, project.ProjectName,
|
||||
p.BookID, book.BookTitle, book.Subtitle AS BookSubtitle,
|
||||
p.CurrentStage, p.LastCompletedStage, p.CurrentReviewStage, p.Status,
|
||||
p.CompletedUtc, p.LastRunID, p.CreatedUtc, p.UpdatedUtc
|
||||
FROM dbo.StoryIntelligenceBookPipelines p
|
||||
INNER JOIN dbo.Projects project ON project.ProjectID = p.ProjectID
|
||||
INNER JOIN dbo.Books book ON book.BookID = p.BookID
|
||||
INNER JOIN dbo.ProjectUserAccess access ON access.ProjectID = p.ProjectID
|
||||
AND access.UserID = @UserID
|
||||
AND access.IsActive = 1
|
||||
WHERE p.BookID = @BookID
|
||||
AND book.IsArchived = 0
|
||||
AND project.IsArchived = 0;
|
||||
END;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.StoryIntelligenceBookPipeline_ListForUser
|
||||
@UserID int
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
SELECT p.StoryIntelligenceBookPipelineID, p.ProjectID, project.ProjectName,
|
||||
p.BookID, book.BookTitle, book.Subtitle AS BookSubtitle,
|
||||
p.CurrentStage, p.LastCompletedStage, p.CurrentReviewStage, p.Status,
|
||||
p.CompletedUtc, p.LastRunID, p.CreatedUtc, p.UpdatedUtc
|
||||
FROM dbo.StoryIntelligenceBookPipelines p
|
||||
INNER JOIN dbo.Projects project ON project.ProjectID = p.ProjectID
|
||||
INNER JOIN dbo.Books book ON book.BookID = p.BookID
|
||||
INNER JOIN dbo.ProjectUserAccess access ON access.ProjectID = p.ProjectID
|
||||
AND access.UserID = @UserID
|
||||
AND access.IsActive = 1
|
||||
WHERE book.IsArchived = 0
|
||||
AND project.IsArchived = 0
|
||||
ORDER BY
|
||||
CASE p.Status WHEN N'NeedsReview' THEN 0 WHEN N'InProgress' THEN 1 ELSE 2 END,
|
||||
p.UpdatedUtc DESC;
|
||||
END;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.StoryIntelligenceBookPipeline_ListCommittedRunsByBook
|
||||
@BookID int,
|
||||
@UserID int
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
SELECT r.StoryIntelligenceRunID, r.UserID,
|
||||
CAST(r.ProjectID AS int) AS ProjectID, project.ProjectName,
|
||||
CAST(r.BookID AS int) AS BookID, book.BookTitle, book.Subtitle AS BookSubtitle,
|
||||
CAST(r.ChapterID AS int) AS ChapterID,
|
||||
CAST(COALESCE(r.ChapterNumber, chapter.ChapterNumber) AS decimal(10,2)) AS ChapterNumber,
|
||||
chapter.ChapterTitle,
|
||||
r.SourceLabel
|
||||
FROM dbo.StoryIntelligenceRuns r
|
||||
INNER JOIN dbo.StoryIntelligenceImportCommits commitRows ON commitRows.StoryIntelligenceRunID = r.StoryIntelligenceRunID
|
||||
AND commitRows.Status = N'Completed'
|
||||
INNER JOIN dbo.Books book ON book.BookID = r.BookID
|
||||
INNER JOIN dbo.Projects project ON project.ProjectID = r.ProjectID
|
||||
INNER JOIN dbo.Chapters chapter ON chapter.ChapterID = r.ChapterID
|
||||
INNER JOIN dbo.ProjectUserAccess access ON access.ProjectID = project.ProjectID
|
||||
AND access.UserID = @UserID
|
||||
AND access.IsActive = 1
|
||||
WHERE r.BookID = @BookID
|
||||
AND r.ProjectID IS NOT NULL
|
||||
AND r.ChapterID IS NOT NULL
|
||||
AND r.BookID IS NOT NULL
|
||||
AND book.IsArchived = 0
|
||||
AND project.IsArchived = 0
|
||||
AND chapter.IsArchived = 0
|
||||
ORDER BY chapter.SortOrder, chapter.ChapterNumber, r.StoryIntelligenceRunID;
|
||||
END;
|
||||
GO
|
||||
@ -244,6 +244,7 @@ public sealed class BookDetailViewModel
|
||||
public Book Book { get; set; } = new();
|
||||
public ManuscriptDocumentModel? ManuscriptDocument { get; set; }
|
||||
public IReadOnlyList<Chapter> Chapters { get; set; } = [];
|
||||
public StoryIntelligenceBookPipelineState? StoryIntelligencePipeline { get; set; }
|
||||
}
|
||||
|
||||
public sealed class ChapterEditViewModel
|
||||
|
||||
@ -294,7 +294,18 @@ public sealed class StoryIntelligenceDashboardViewModel
|
||||
public Guid? ActiveBatchID { get; init; }
|
||||
public string? ActiveImportBookTitle { get; init; }
|
||||
public string? ActiveImportProgressLabel { get; init; }
|
||||
public IReadOnlyList<StoryIntelligencePipelineBookStatusViewModel> PipelineBooks { get; init; } = [];
|
||||
public bool HasActiveImport => ActiveBatchID.HasValue;
|
||||
public bool HasPipelineBooks => PipelineBooks.Count > 0;
|
||||
}
|
||||
|
||||
public sealed class StoryIntelligencePipelineBookStatusViewModel
|
||||
{
|
||||
public int BookID { get; init; }
|
||||
public string BookTitle { get; init; } = string.Empty;
|
||||
public string StatusLabel { get; init; } = "Story Intelligence Needs Review";
|
||||
public string Description { get; init; } = string.Empty;
|
||||
public bool IsComplete { get; init; }
|
||||
}
|
||||
|
||||
public sealed class CompanionPresenceViewModel
|
||||
|
||||
@ -58,6 +58,24 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (Model.StoryIntelligencePipeline is not null)
|
||||
{
|
||||
var pipeline = Model.StoryIntelligencePipeline;
|
||||
<section class="list-section">
|
||||
<div class="d-flex flex-wrap align-items-center justify-content-between gap-3">
|
||||
<div>
|
||||
<p class="eyebrow">Story Intelligence</p>
|
||||
<h2>@(pipeline.IsComplete ? "Story Intelligence Complete" : "Continue Story Intelligence")</h2>
|
||||
<p class="muted mb-0">@StoryIntelligenceDescription(pipeline)</p>
|
||||
</div>
|
||||
<a class="btn btn-primary"
|
||||
asp-controller="Onboarding"
|
||||
asp-action="ContinueStoryIntelligence"
|
||||
asp-route-bookId="@Model.Book.BookID">@(pipeline.IsComplete ? "View Story Intelligence Summary" : "Continue Story Intelligence")</a>
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
|
||||
<section class="list-section">
|
||||
<h2>Book metadata</h2>
|
||||
<div class="row g-3">
|
||||
@ -171,3 +189,20 @@
|
||||
</div>
|
||||
}
|
||||
</section>
|
||||
|
||||
@functions {
|
||||
private static string StoryIntelligenceDescription(StoryIntelligenceBookPipelineState pipeline)
|
||||
{
|
||||
if (pipeline.IsComplete)
|
||||
{
|
||||
return "All currently available Story Intelligence stages are complete.";
|
||||
}
|
||||
|
||||
return pipeline.CurrentReviewStage switch
|
||||
{
|
||||
StoryIntelligencePipelineStages.CharacterReview => "Scene creation is complete. Review detected characters to continue building the story database.",
|
||||
StoryIntelligencePipelineStages.SceneReview => "Review prepared scenes before creating them in PlotDirector.",
|
||||
_ => "PlotDirector will resume from the next Story Intelligence stage."
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -52,6 +52,32 @@
|
||||
asp-route-batchId="@Model.StoryIntelligence.ActiveBatchID">View progress</a>
|
||||
</section>
|
||||
}
|
||||
else if (Model.StoryIntelligence.HasPipelineBooks)
|
||||
{
|
||||
var primaryPipelineBook = Model.StoryIntelligence.PipelineBooks.FirstOrDefault(item => !item.IsComplete)
|
||||
?? Model.StoryIntelligence.PipelineBooks.First();
|
||||
<section class="story-intelligence-dashboard-card story-intelligence-dashboard-card--compact"
|
||||
aria-label="Story Intelligence status">
|
||||
<div>
|
||||
<p class="eyebrow">Story Intelligence</p>
|
||||
<h2>@Model.StoryIntelligence.Title</h2>
|
||||
<p>@Model.StoryIntelligence.Description</p>
|
||||
<div class="story-intelligence-status-list">
|
||||
@foreach (var item in Model.StoryIntelligence.PipelineBooks.Take(3))
|
||||
{
|
||||
<div class="story-intelligence-status-list__item">
|
||||
<strong>@item.BookTitle</strong>
|
||||
<span>@item.StatusLabel</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<a class="btn btn-primary"
|
||||
asp-controller="Onboarding"
|
||||
asp-action="ContinueStoryIntelligence"
|
||||
asp-route-bookId="@primaryPipelineBook.BookID">@Model.StoryIntelligence.ButtonText</a>
|
||||
</section>
|
||||
}
|
||||
|
||||
@if (TempData["ArchiveMessage"] is string archiveMessage)
|
||||
{
|
||||
|
||||
@ -1261,6 +1261,24 @@ summary.story-review-chapter-heading {
|
||||
color: var(--bs-secondary-color);
|
||||
}
|
||||
|
||||
.story-intelligence-status-list {
|
||||
display: grid;
|
||||
gap: .35rem;
|
||||
margin-top: .7rem;
|
||||
}
|
||||
|
||||
.story-intelligence-status-list__item {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: .35rem .75rem;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.story-intelligence-status-list__item span {
|
||||
color: var(--bs-secondary-color);
|
||||
font-size: .9rem;
|
||||
}
|
||||
|
||||
.story-intelligence-dashboard-progress {
|
||||
display: grid;
|
||||
grid-template-columns: auto auto;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user