Phase 20Q — Real Chapter Source From Existing Book
This commit is contained in:
parent
9f9688bcb5
commit
7ccf334c93
@ -16,7 +16,8 @@ public sealed class AdminController(
|
|||||||
IStoryIntelligenceDryRunService storyIntelligenceDryRun,
|
IStoryIntelligenceDryRunService storyIntelligenceDryRun,
|
||||||
IChapterStructureDryRunService chapterStructureDryRun,
|
IChapterStructureDryRunService chapterStructureDryRun,
|
||||||
IChapterStoryIntelligenceDryRunService chapterStoryIntelligenceDryRun,
|
IChapterStoryIntelligenceDryRunService chapterStoryIntelligenceDryRun,
|
||||||
IStoryIntelligenceResultPersistenceService storyIntelligenceResults) : Controller
|
IStoryIntelligenceResultPersistenceService storyIntelligenceResults,
|
||||||
|
IStoryIntelligenceExistingChapterQueueService existingChapterQueue) : Controller
|
||||||
{
|
{
|
||||||
[HttpGet("")]
|
[HttpGet("")]
|
||||||
[HttpGet("index")]
|
[HttpGet("index")]
|
||||||
@ -107,6 +108,31 @@ public sealed class AdminController(
|
|||||||
return RedirectToAction(nameof(StoryIntelligenceRunDetails), new { id = runId });
|
return RedirectToAction(nameof(StoryIntelligenceRunDetails), new { id = runId });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet("story-intelligence-existing-chapter")]
|
||||||
|
public async Task<IActionResult> StoryIntelligenceExistingChapter([FromQuery] StoryIntelligenceExistingChapterQueueForm form)
|
||||||
|
{
|
||||||
|
return View(await existingChapterQueue.BuildViewModelAsync(form));
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("story-intelligence-existing-chapter")]
|
||||||
|
[ValidateAntiForgeryToken]
|
||||||
|
public async Task<IActionResult> QueueStoryIntelligenceExistingChapter(StoryIntelligenceExistingChapterQueueForm form)
|
||||||
|
{
|
||||||
|
if (currentUser.UserId is not int userId)
|
||||||
|
{
|
||||||
|
return Forbid();
|
||||||
|
}
|
||||||
|
|
||||||
|
var model = await existingChapterQueue.QueueAsync(userId, form);
|
||||||
|
if (model.QueuedRunID is int runId)
|
||||||
|
{
|
||||||
|
TempData["AdminMessage"] = $"Story Intelligence run {runId:N0} queued from existing chapter. The background worker will process it progressively.";
|
||||||
|
return RedirectToAction(nameof(StoryIntelligenceRunDetails), new { id = runId });
|
||||||
|
}
|
||||||
|
|
||||||
|
return View(nameof(StoryIntelligenceExistingChapter), model);
|
||||||
|
}
|
||||||
|
|
||||||
[HttpGet("story-intelligence-runs")]
|
[HttpGet("story-intelligence-runs")]
|
||||||
public async Task<IActionResult> StoryIntelligenceRuns()
|
public async Task<IActionResult> StoryIntelligenceRuns()
|
||||||
{
|
{
|
||||||
|
|||||||
@ -163,6 +163,8 @@ public sealed class StoryIntelligenceResultRepository(ISqlConnectionFactory conn
|
|||||||
request.UserID,
|
request.UserID,
|
||||||
request.ProjectID,
|
request.ProjectID,
|
||||||
request.BookID,
|
request.BookID,
|
||||||
|
request.ChapterID,
|
||||||
|
request.ChapterNumber,
|
||||||
request.SourceType,
|
request.SourceType,
|
||||||
request.SourceLabel,
|
request.SourceLabel,
|
||||||
SourceText = request.ChapterText,
|
SourceText = request.ChapterText,
|
||||||
|
|||||||
54
PlotLine/Data/StoryIntelligenceSourceRepository.cs
Normal file
54
PlotLine/Data/StoryIntelligenceSourceRepository.cs
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
using System.Data;
|
||||||
|
using Dapper;
|
||||||
|
using PlotLine.Models;
|
||||||
|
|
||||||
|
namespace PlotLine.Data;
|
||||||
|
|
||||||
|
public interface IStoryIntelligenceSourceRepository
|
||||||
|
{
|
||||||
|
Task<IReadOnlyList<StoryIntelligenceSourceOption>> ListProjectsAsync();
|
||||||
|
Task<IReadOnlyList<StoryIntelligenceSourceOption>> ListBooksAsync(int projectId);
|
||||||
|
Task<IReadOnlyList<StoryIntelligenceSourceOption>> ListChaptersAsync(int bookId);
|
||||||
|
Task<StoryIntelligenceExistingChapterSource?> GetChapterSourceAsync(int chapterId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class StoryIntelligenceSourceRepository(ISqlConnectionFactory connectionFactory) : IStoryIntelligenceSourceRepository
|
||||||
|
{
|
||||||
|
public async Task<IReadOnlyList<StoryIntelligenceSourceOption>> ListProjectsAsync()
|
||||||
|
{
|
||||||
|
using var connection = connectionFactory.CreateConnection();
|
||||||
|
var rows = await connection.QueryAsync<StoryIntelligenceSourceOption>(
|
||||||
|
"dbo.StoryIntelligenceSource_ProjectListAdmin",
|
||||||
|
commandType: CommandType.StoredProcedure);
|
||||||
|
return rows.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<StoryIntelligenceSourceOption>> ListBooksAsync(int projectId)
|
||||||
|
{
|
||||||
|
using var connection = connectionFactory.CreateConnection();
|
||||||
|
var rows = await connection.QueryAsync<StoryIntelligenceSourceOption>(
|
||||||
|
"dbo.StoryIntelligenceSource_BookListAdmin",
|
||||||
|
new { ProjectID = projectId },
|
||||||
|
commandType: CommandType.StoredProcedure);
|
||||||
|
return rows.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<StoryIntelligenceSourceOption>> ListChaptersAsync(int bookId)
|
||||||
|
{
|
||||||
|
using var connection = connectionFactory.CreateConnection();
|
||||||
|
var rows = await connection.QueryAsync<StoryIntelligenceSourceOption>(
|
||||||
|
"dbo.StoryIntelligenceSource_ChapterListAdmin",
|
||||||
|
new { BookID = bookId },
|
||||||
|
commandType: CommandType.StoredProcedure);
|
||||||
|
return rows.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<StoryIntelligenceExistingChapterSource?> GetChapterSourceAsync(int chapterId)
|
||||||
|
{
|
||||||
|
using var connection = connectionFactory.CreateConnection();
|
||||||
|
return await connection.QuerySingleOrDefaultAsync<StoryIntelligenceExistingChapterSource>(
|
||||||
|
"dbo.StoryIntelligenceSource_ChapterText_GetAdmin",
|
||||||
|
new { ChapterID = chapterId },
|
||||||
|
commandType: CommandType.StoredProcedure);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -60,6 +60,8 @@ public sealed class StoryIntelligenceRunQueueRequest
|
|||||||
public int UserID { get; init; }
|
public int UserID { get; init; }
|
||||||
public int? ProjectID { get; init; }
|
public int? ProjectID { get; init; }
|
||||||
public int? BookID { get; init; }
|
public int? BookID { get; init; }
|
||||||
|
public int? ChapterID { get; init; }
|
||||||
|
public decimal? ChapterNumber { get; init; }
|
||||||
public string SourceType { get; init; } = "AdminText";
|
public string SourceType { get; init; } = "AdminText";
|
||||||
public string SourceLabel { get; init; } = string.Empty;
|
public string SourceLabel { get; init; } = string.Empty;
|
||||||
public string ChapterText { get; init; } = string.Empty;
|
public string ChapterText { get; init; } = string.Empty;
|
||||||
@ -122,6 +124,8 @@ public sealed class StoryIntelligenceSavedRunListItem
|
|||||||
public string? ProjectTitle { get; init; }
|
public string? ProjectTitle { get; init; }
|
||||||
public int? BookID { get; init; }
|
public int? BookID { get; init; }
|
||||||
public string? BookTitle { get; init; }
|
public string? BookTitle { get; init; }
|
||||||
|
public int? ChapterID { get; init; }
|
||||||
|
public decimal? ChapterNumber { get; init; }
|
||||||
public string Status { get; init; } = string.Empty;
|
public string Status { get; init; } = string.Empty;
|
||||||
public string SourceType { get; init; } = string.Empty;
|
public string SourceType { get; init; } = string.Empty;
|
||||||
public string? FailureStage { get; init; }
|
public string? FailureStage { get; init; }
|
||||||
@ -142,6 +146,8 @@ public sealed class StoryIntelligenceSavedRun
|
|||||||
public string? ProjectTitle { get; init; }
|
public string? ProjectTitle { get; init; }
|
||||||
public int? BookID { get; init; }
|
public int? BookID { get; init; }
|
||||||
public string? BookTitle { get; init; }
|
public string? BookTitle { get; init; }
|
||||||
|
public int? ChapterID { get; init; }
|
||||||
|
public decimal? ChapterNumber { get; init; }
|
||||||
public string Status { get; init; } = string.Empty;
|
public string Status { get; init; } = string.Empty;
|
||||||
public string SourceType { get; init; } = string.Empty;
|
public string SourceType { get; init; } = string.Empty;
|
||||||
public string? SourceFileName { get; init; }
|
public string? SourceFileName { get; init; }
|
||||||
@ -184,6 +190,8 @@ public sealed class StoryIntelligenceQueuedRun
|
|||||||
public int UserID { get; init; }
|
public int UserID { get; init; }
|
||||||
public int? ProjectID { get; init; }
|
public int? ProjectID { get; init; }
|
||||||
public int? BookID { get; init; }
|
public int? BookID { get; init; }
|
||||||
|
public int? ChapterID { get; init; }
|
||||||
|
public decimal? ChapterNumber { get; init; }
|
||||||
public string Status { get; init; } = string.Empty;
|
public string Status { get; init; } = string.Empty;
|
||||||
public string SourceType { get; init; } = string.Empty;
|
public string SourceType { get; init; } = string.Empty;
|
||||||
public string? SourceLabel { get; init; }
|
public string? SourceLabel { get; init; }
|
||||||
@ -217,6 +225,48 @@ public sealed class StoryIntelligenceQueuedRun
|
|||||||
public bool CancellationRequested => CancellationRequestedUtc.HasValue;
|
public bool CancellationRequested => CancellationRequestedUtc.HasValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public sealed class StoryIntelligenceSourceOption
|
||||||
|
{
|
||||||
|
public int Value { get; init; }
|
||||||
|
public string Text { get; init; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class StoryIntelligenceExistingChapterSource
|
||||||
|
{
|
||||||
|
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? SourceText { get; init; }
|
||||||
|
public int? SourceWordCount { get; init; }
|
||||||
|
public int? SourceCharacterCount { get; init; }
|
||||||
|
|
||||||
|
public bool HasSourceText => !string.IsNullOrWhiteSpace(SourceText);
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class StoryIntelligenceExistingChapterQueueForm
|
||||||
|
{
|
||||||
|
public int? ProjectID { get; init; }
|
||||||
|
public int? BookID { get; init; }
|
||||||
|
public int? ChapterID { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class StoryIntelligenceExistingChapterQueueViewModel
|
||||||
|
{
|
||||||
|
public StoryIntelligenceExistingChapterQueueForm Form { get; init; } = new();
|
||||||
|
public IReadOnlyList<StoryIntelligenceSourceOption> Projects { get; init; } = [];
|
||||||
|
public IReadOnlyList<StoryIntelligenceSourceOption> Books { get; init; } = [];
|
||||||
|
public IReadOnlyList<StoryIntelligenceSourceOption> Chapters { get; init; } = [];
|
||||||
|
public StoryIntelligenceExistingChapterSource? SelectedChapter { get; init; }
|
||||||
|
public string? WarningMessage { get; init; }
|
||||||
|
public string? ErrorMessage { get; init; }
|
||||||
|
public int? QueuedRunID { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
public sealed class StoryIntelligenceSavedChapterResult
|
public sealed class StoryIntelligenceSavedChapterResult
|
||||||
{
|
{
|
||||||
public int ChapterResultID { get; init; }
|
public int ChapterResultID { get; init; }
|
||||||
|
|||||||
@ -138,6 +138,7 @@ public class Program
|
|||||||
builder.Services.AddScoped<IOnboardingBuildRepository, OnboardingBuildRepository>();
|
builder.Services.AddScoped<IOnboardingBuildRepository, OnboardingBuildRepository>();
|
||||||
builder.Services.AddScoped<IStoryIntelligenceRepository, StoryIntelligenceRepository>();
|
builder.Services.AddScoped<IStoryIntelligenceRepository, StoryIntelligenceRepository>();
|
||||||
builder.Services.AddScoped<IStoryIntelligenceResultRepository, StoryIntelligenceResultRepository>();
|
builder.Services.AddScoped<IStoryIntelligenceResultRepository, StoryIntelligenceResultRepository>();
|
||||||
|
builder.Services.AddScoped<IStoryIntelligenceSourceRepository, StoryIntelligenceSourceRepository>();
|
||||||
builder.Services.AddScoped<IProjectAccessRepository, ProjectAccessRepository>();
|
builder.Services.AddScoped<IProjectAccessRepository, ProjectAccessRepository>();
|
||||||
builder.Services.AddScoped<IProjectCollaborationRepository, ProjectCollaborationRepository>();
|
builder.Services.AddScoped<IProjectCollaborationRepository, ProjectCollaborationRepository>();
|
||||||
builder.Services.AddScoped<IProjectActivityRepository, ProjectActivityRepository>();
|
builder.Services.AddScoped<IProjectActivityRepository, ProjectActivityRepository>();
|
||||||
@ -196,6 +197,7 @@ public class Program
|
|||||||
builder.Services.AddScoped<IChapterStructureDryRunService, ChapterStructureDryRunService>();
|
builder.Services.AddScoped<IChapterStructureDryRunService, ChapterStructureDryRunService>();
|
||||||
builder.Services.AddScoped<IChapterStoryIntelligenceDryRunService, ChapterStoryIntelligenceDryRunService>();
|
builder.Services.AddScoped<IChapterStoryIntelligenceDryRunService, ChapterStoryIntelligenceDryRunService>();
|
||||||
builder.Services.AddScoped<IStoryIntelligenceResultPersistenceService, StoryIntelligenceResultPersistenceService>();
|
builder.Services.AddScoped<IStoryIntelligenceResultPersistenceService, StoryIntelligenceResultPersistenceService>();
|
||||||
|
builder.Services.AddScoped<IStoryIntelligenceExistingChapterQueueService, StoryIntelligenceExistingChapterQueueService>();
|
||||||
builder.Services.AddScoped<IPersistedStoryIntelligenceRunner, PersistedStoryIntelligenceRunner>();
|
builder.Services.AddScoped<IPersistedStoryIntelligenceRunner, PersistedStoryIntelligenceRunner>();
|
||||||
builder.Services.AddScoped<IProjectAccessService, ProjectAccessService>();
|
builder.Services.AddScoped<IProjectAccessService, ProjectAccessService>();
|
||||||
builder.Services.AddScoped<ISubscriptionService, SubscriptionService>();
|
builder.Services.AddScoped<ISubscriptionService, SubscriptionService>();
|
||||||
|
|||||||
@ -95,8 +95,8 @@ public sealed class PersistedStoryIntelligenceRunner(
|
|||||||
{
|
{
|
||||||
ProjectID = run.ProjectID,
|
ProjectID = run.ProjectID,
|
||||||
BookID = run.BookID,
|
BookID = run.BookID,
|
||||||
ChapterID = null,
|
ChapterID = run.ChapterID,
|
||||||
ChapterNumber = 1,
|
ChapterNumber = ChapterNumber(run),
|
||||||
SourceLabel = SourceLabel(run),
|
SourceLabel = SourceLabel(run),
|
||||||
PromptVersion = chapterPromptVersion,
|
PromptVersion = chapterPromptVersion,
|
||||||
Model = chapterClientResult.Model,
|
Model = chapterClientResult.Model,
|
||||||
@ -172,7 +172,7 @@ public sealed class PersistedStoryIntelligenceRunner(
|
|||||||
{
|
{
|
||||||
ProjectID = run.ProjectID,
|
ProjectID = run.ProjectID,
|
||||||
BookID = run.BookID,
|
BookID = run.BookID,
|
||||||
ChapterID = null,
|
ChapterID = run.ChapterID,
|
||||||
SceneID = null,
|
SceneID = null,
|
||||||
TemporarySceneNumber = block.TemporarySceneNumber,
|
TemporarySceneNumber = block.TemporarySceneNumber,
|
||||||
StartParagraph = block.StartParagraph,
|
StartParagraph = block.StartParagraph,
|
||||||
@ -274,7 +274,7 @@ public sealed class PersistedStoryIntelligenceRunner(
|
|||||||
{
|
{
|
||||||
ProjectID = run.ProjectID,
|
ProjectID = run.ProjectID,
|
||||||
BookID = run.BookID,
|
BookID = run.BookID,
|
||||||
ChapterID = null,
|
ChapterID = run.ChapterID,
|
||||||
SceneID = null,
|
SceneID = null,
|
||||||
TemporarySceneNumber = block.TemporarySceneNumber,
|
TemporarySceneNumber = block.TemporarySceneNumber,
|
||||||
StartParagraph = block.StartParagraph,
|
StartParagraph = block.StartParagraph,
|
||||||
@ -336,8 +336,8 @@ public sealed class PersistedStoryIntelligenceRunner(
|
|||||||
{
|
{
|
||||||
projectId = run.ProjectID,
|
projectId = run.ProjectID,
|
||||||
bookId = run.BookID,
|
bookId = run.BookID,
|
||||||
chapterId = (int?)null,
|
chapterId = run.ChapterID,
|
||||||
chapterNumber = 1,
|
chapterNumber = ChapterNumber(run),
|
||||||
sourceLabel = SourceLabel(run)
|
sourceLabel = SourceLabel(run)
|
||||||
}, JsonOptions);
|
}, JsonOptions);
|
||||||
|
|
||||||
@ -346,9 +346,9 @@ public sealed class PersistedStoryIntelligenceRunner(
|
|||||||
{
|
{
|
||||||
projectId = run.ProjectID,
|
projectId = run.ProjectID,
|
||||||
bookId = run.BookID,
|
bookId = run.BookID,
|
||||||
chapterId = (int?)null,
|
chapterId = run.ChapterID,
|
||||||
sceneId = (int?)null,
|
sceneId = (int?)null,
|
||||||
chapterNumber = 1,
|
chapterNumber = ChapterNumber(run),
|
||||||
sceneNumber = boundary.SceneNumber,
|
sceneNumber = boundary.SceneNumber,
|
||||||
sourceLabel = $"{SourceLabel(run)}, suggested scene {boundary.SceneNumber}"
|
sourceLabel = $"{SourceLabel(run)}, suggested scene {boundary.SceneNumber}"
|
||||||
}, JsonOptions);
|
}, JsonOptions);
|
||||||
@ -388,6 +388,9 @@ public sealed class PersistedStoryIntelligenceRunner(
|
|||||||
private static string SourceLabel(StoryIntelligenceQueuedRun run)
|
private static string SourceLabel(StoryIntelligenceQueuedRun run)
|
||||||
=> string.IsNullOrWhiteSpace(run.SourceLabel) ? "Admin persisted Story Intelligence chapter" : run.SourceLabel.Trim();
|
=> string.IsNullOrWhiteSpace(run.SourceLabel) ? "Admin persisted Story Intelligence chapter" : run.SourceLabel.Trim();
|
||||||
|
|
||||||
|
private static decimal ChapterNumber(StoryIntelligenceQueuedRun run)
|
||||||
|
=> run.ChapterNumber ?? 1;
|
||||||
|
|
||||||
private static string BuildValidationDetail(PlotLine.Models.StoryIntelligence.ValidationResult validation)
|
private static string BuildValidationDetail(PlotLine.Models.StoryIntelligence.ValidationResult validation)
|
||||||
=> string.Join(
|
=> string.Join(
|
||||||
Environment.NewLine,
|
Environment.NewLine,
|
||||||
|
|||||||
@ -0,0 +1,145 @@
|
|||||||
|
using PlotLine.Data;
|
||||||
|
using PlotLine.Models;
|
||||||
|
|
||||||
|
namespace PlotLine.Services;
|
||||||
|
|
||||||
|
public interface IStoryIntelligenceExistingChapterQueueService
|
||||||
|
{
|
||||||
|
Task<StoryIntelligenceExistingChapterQueueViewModel> BuildViewModelAsync(StoryIntelligenceExistingChapterQueueForm form);
|
||||||
|
Task<StoryIntelligenceExistingChapterQueueViewModel> QueueAsync(int userId, StoryIntelligenceExistingChapterQueueForm form);
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class StoryIntelligenceExistingChapterQueueService(
|
||||||
|
IStoryIntelligenceSourceRepository sources,
|
||||||
|
IStoryIntelligenceResultRepository runs,
|
||||||
|
IStoryIntelligenceClient client,
|
||||||
|
ILogger<StoryIntelligenceExistingChapterQueueService> logger) : IStoryIntelligenceExistingChapterQueueService
|
||||||
|
{
|
||||||
|
private const string NoStoredChapterTextMessage = "No stored chapter text is currently available for this chapter. A future Word Companion/document extraction phase is required.";
|
||||||
|
|
||||||
|
public async Task<StoryIntelligenceExistingChapterQueueViewModel> BuildViewModelAsync(StoryIntelligenceExistingChapterQueueForm form)
|
||||||
|
{
|
||||||
|
var selected = await GetSelectedChapterAsync(form);
|
||||||
|
return await BuildViewModelAsync(form, selected, warningMessage: BuildWarning(selected), errorMessage: null, queuedRunId: null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<StoryIntelligenceExistingChapterQueueViewModel> QueueAsync(int userId, StoryIntelligenceExistingChapterQueueForm form)
|
||||||
|
{
|
||||||
|
if (form.ChapterID is not int chapterId)
|
||||||
|
{
|
||||||
|
return await BuildViewModelAsync(
|
||||||
|
form,
|
||||||
|
selectedChapter: null,
|
||||||
|
warningMessage: null,
|
||||||
|
errorMessage: "Choose a chapter before queueing a Story Intelligence run.",
|
||||||
|
queuedRunId: null);
|
||||||
|
}
|
||||||
|
|
||||||
|
var selected = await sources.GetChapterSourceAsync(chapterId);
|
||||||
|
if (selected is null)
|
||||||
|
{
|
||||||
|
return await BuildViewModelAsync(
|
||||||
|
form,
|
||||||
|
selectedChapter: null,
|
||||||
|
warningMessage: null,
|
||||||
|
errorMessage: "The selected chapter could not be found.",
|
||||||
|
queuedRunId: null);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!selected.HasSourceText)
|
||||||
|
{
|
||||||
|
return await BuildViewModelAsync(
|
||||||
|
NormalizeForm(selected),
|
||||||
|
selected,
|
||||||
|
NoStoredChapterTextMessage,
|
||||||
|
errorMessage: null,
|
||||||
|
queuedRunId: null);
|
||||||
|
}
|
||||||
|
|
||||||
|
var sourceText = selected.SourceText!.Trim();
|
||||||
|
var clientStatus = client.GetConfigurationStatus();
|
||||||
|
var runId = await runs.QueueAdminTextAsync(new StoryIntelligenceRunQueueRequest
|
||||||
|
{
|
||||||
|
UserID = userId,
|
||||||
|
ProjectID = selected.ProjectID,
|
||||||
|
BookID = selected.BookID,
|
||||||
|
ChapterID = selected.ChapterID,
|
||||||
|
ChapterNumber = selected.ChapterNumber,
|
||||||
|
SourceType = "ExistingChapter",
|
||||||
|
SourceLabel = BuildSourceLabel(selected),
|
||||||
|
ChapterText = sourceText,
|
||||||
|
SourceWordCount = selected.SourceWordCount ?? CountWords(sourceText),
|
||||||
|
SourceCharacterCount = selected.SourceCharacterCount ?? sourceText.Length,
|
||||||
|
SourceChapterCount = 1,
|
||||||
|
Model = clientStatus.Model,
|
||||||
|
PromptVersionsSummary = "Chapter=Chapter-Structure-Prompt-V1; Scene=Scene-Prompt-V1"
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.LogInformation(
|
||||||
|
"Queued Story Intelligence run {StoryIntelligenceRunID} from existing chapter {ChapterID}. ProjectID={ProjectID} BookID={BookID} SourceWordCount={SourceWordCount} SourceCharacterCount={SourceCharacterCount}",
|
||||||
|
runId,
|
||||||
|
selected.ChapterID,
|
||||||
|
selected.ProjectID,
|
||||||
|
selected.BookID,
|
||||||
|
selected.SourceWordCount,
|
||||||
|
selected.SourceCharacterCount);
|
||||||
|
|
||||||
|
return await BuildViewModelAsync(NormalizeForm(selected), selected, warningMessage: null, errorMessage: null, queuedRunId: runId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<StoryIntelligenceExistingChapterQueueViewModel> BuildViewModelAsync(
|
||||||
|
StoryIntelligenceExistingChapterQueueForm form,
|
||||||
|
StoryIntelligenceExistingChapterSource? selectedChapter,
|
||||||
|
string? warningMessage,
|
||||||
|
string? errorMessage,
|
||||||
|
int? queuedRunId)
|
||||||
|
{
|
||||||
|
var normalizedForm = selectedChapter is not null ? NormalizeForm(selectedChapter) : form;
|
||||||
|
var projects = await sources.ListProjectsAsync();
|
||||||
|
var books = normalizedForm.ProjectID is int projectId
|
||||||
|
? await sources.ListBooksAsync(projectId)
|
||||||
|
: [];
|
||||||
|
var chapters = normalizedForm.BookID is int bookId
|
||||||
|
? await sources.ListChaptersAsync(bookId)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
return new StoryIntelligenceExistingChapterQueueViewModel
|
||||||
|
{
|
||||||
|
Form = normalizedForm,
|
||||||
|
Projects = projects,
|
||||||
|
Books = books,
|
||||||
|
Chapters = chapters,
|
||||||
|
SelectedChapter = selectedChapter,
|
||||||
|
WarningMessage = warningMessage,
|
||||||
|
ErrorMessage = errorMessage,
|
||||||
|
QueuedRunID = queuedRunId
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<StoryIntelligenceExistingChapterSource?> GetSelectedChapterAsync(StoryIntelligenceExistingChapterQueueForm form)
|
||||||
|
=> form.ChapterID is int chapterId ? await sources.GetChapterSourceAsync(chapterId) : null;
|
||||||
|
|
||||||
|
private static string? BuildWarning(StoryIntelligenceExistingChapterSource? selected)
|
||||||
|
=> selected is not null && !selected.HasSourceText ? NoStoredChapterTextMessage : null;
|
||||||
|
|
||||||
|
private static StoryIntelligenceExistingChapterQueueForm NormalizeForm(StoryIntelligenceExistingChapterSource selected)
|
||||||
|
=> new()
|
||||||
|
{
|
||||||
|
ProjectID = selected.ProjectID,
|
||||||
|
BookID = selected.BookID,
|
||||||
|
ChapterID = selected.ChapterID
|
||||||
|
};
|
||||||
|
|
||||||
|
private static string BuildSourceLabel(StoryIntelligenceExistingChapterSource selected)
|
||||||
|
{
|
||||||
|
var book = string.IsNullOrWhiteSpace(selected.BookSubtitle)
|
||||||
|
? selected.BookTitle
|
||||||
|
: $"{selected.BookTitle}: {selected.BookSubtitle}";
|
||||||
|
return $"{selected.ProjectName} / {book} / Chapter {selected.ChapterNumber}: {selected.ChapterTitle}";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int? CountWords(string? value)
|
||||||
|
=> string.IsNullOrWhiteSpace(value)
|
||||||
|
? null
|
||||||
|
: value.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries).Length;
|
||||||
|
}
|
||||||
@ -74,6 +74,8 @@ public sealed class StoryIntelligenceResultPersistenceService(
|
|||||||
UserID = userId,
|
UserID = userId,
|
||||||
ProjectID = form.ProjectID,
|
ProjectID = form.ProjectID,
|
||||||
BookID = form.BookID,
|
BookID = form.BookID,
|
||||||
|
ChapterID = form.ChapterID,
|
||||||
|
ChapterNumber = form.ChapterNumber,
|
||||||
SourceType = "AdminText",
|
SourceType = "AdminText",
|
||||||
SourceLabel = CleanSourceLabel(form.SourceLabel),
|
SourceLabel = CleanSourceLabel(form.SourceLabel),
|
||||||
ChapterText = form.ChapterText,
|
ChapterText = form.ChapterText,
|
||||||
|
|||||||
@ -0,0 +1,281 @@
|
|||||||
|
IF COL_LENGTH(N'dbo.StoryIntelligenceRuns', N'ChapterID') IS NULL
|
||||||
|
BEGIN
|
||||||
|
ALTER TABLE dbo.StoryIntelligenceRuns ADD ChapterID int NULL;
|
||||||
|
END;
|
||||||
|
GO
|
||||||
|
|
||||||
|
IF COL_LENGTH(N'dbo.StoryIntelligenceRuns', N'ChapterNumber') IS NULL
|
||||||
|
BEGIN
|
||||||
|
ALTER TABLE dbo.StoryIntelligenceRuns ADD ChapterNumber decimal(9, 2) NULL;
|
||||||
|
END;
|
||||||
|
GO
|
||||||
|
|
||||||
|
IF NOT EXISTS
|
||||||
|
(
|
||||||
|
SELECT 1
|
||||||
|
FROM sys.foreign_keys
|
||||||
|
WHERE name = N'FK_StoryIntelligenceRuns_Chapters'
|
||||||
|
AND parent_object_id = OBJECT_ID(N'dbo.StoryIntelligenceRuns')
|
||||||
|
)
|
||||||
|
BEGIN
|
||||||
|
ALTER TABLE dbo.StoryIntelligenceRuns
|
||||||
|
ADD CONSTRAINT FK_StoryIntelligenceRuns_Chapters
|
||||||
|
FOREIGN KEY (ChapterID) REFERENCES dbo.Chapters(ChapterID);
|
||||||
|
END;
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE OR ALTER PROCEDURE dbo.StoryIntelligenceSource_ProjectListAdmin
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
ProjectID AS Value,
|
||||||
|
ProjectName AS Text
|
||||||
|
FROM dbo.Projects
|
||||||
|
WHERE IsArchived = 0
|
||||||
|
ORDER BY ProjectName;
|
||||||
|
END;
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE OR ALTER PROCEDURE dbo.StoryIntelligenceSource_BookListAdmin
|
||||||
|
@ProjectID int
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
BookID AS Value,
|
||||||
|
CASE
|
||||||
|
WHEN NULLIF(LTRIM(RTRIM(Subtitle)), N'') IS NULL THEN BookTitle
|
||||||
|
ELSE CONCAT(BookTitle, N': ', Subtitle)
|
||||||
|
END AS Text
|
||||||
|
FROM dbo.Books
|
||||||
|
WHERE ProjectID = @ProjectID
|
||||||
|
AND IsArchived = 0
|
||||||
|
ORDER BY SortOrder, BookNumber, BookTitle;
|
||||||
|
END;
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE OR ALTER PROCEDURE dbo.StoryIntelligenceSource_ChapterListAdmin
|
||||||
|
@BookID int
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
ChapterID AS Value,
|
||||||
|
CONCAT(N'Chapter ', CONVERT(nvarchar(20), ChapterNumber), N': ', ChapterTitle) AS Text
|
||||||
|
FROM dbo.Chapters
|
||||||
|
WHERE BookID = @BookID
|
||||||
|
AND IsArchived = 0
|
||||||
|
ORDER BY SortOrder, ChapterNumber, ChapterTitle;
|
||||||
|
END;
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE OR ALTER PROCEDURE dbo.StoryIntelligenceSource_ChapterText_GetAdmin
|
||||||
|
@ChapterID int
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
p.ProjectID,
|
||||||
|
p.ProjectName,
|
||||||
|
b.BookID,
|
||||||
|
b.BookTitle,
|
||||||
|
b.Subtitle AS BookSubtitle,
|
||||||
|
c.ChapterID,
|
||||||
|
c.ChapterNumber,
|
||||||
|
c.ChapterTitle,
|
||||||
|
CAST(NULL AS nvarchar(max)) AS SourceText,
|
||||||
|
CAST(NULL AS int) AS SourceWordCount,
|
||||||
|
CAST(NULL AS int) AS SourceCharacterCount
|
||||||
|
FROM dbo.Chapters c
|
||||||
|
INNER JOIN dbo.Books b ON b.BookID = c.BookID
|
||||||
|
INNER JOIN dbo.Projects p ON p.ProjectID = b.ProjectID
|
||||||
|
WHERE c.ChapterID = @ChapterID
|
||||||
|
AND c.IsArchived = 0
|
||||||
|
AND b.IsArchived = 0
|
||||||
|
AND p.IsArchived = 0;
|
||||||
|
END;
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE OR ALTER PROCEDURE dbo.StoryIntelligenceRun_QueueAdminText
|
||||||
|
@UserID int,
|
||||||
|
@ProjectID int = NULL,
|
||||||
|
@BookID int = NULL,
|
||||||
|
@ChapterID int = NULL,
|
||||||
|
@ChapterNumber decimal(9, 2) = NULL,
|
||||||
|
@SourceType nvarchar(50),
|
||||||
|
@SourceLabel nvarchar(300),
|
||||||
|
@SourceText nvarchar(max),
|
||||||
|
@SourceWordCount int = NULL,
|
||||||
|
@SourceCharacterCount int = NULL,
|
||||||
|
@SourceChapterCount int = NULL,
|
||||||
|
@Model nvarchar(100),
|
||||||
|
@PromptVersionsSummary nvarchar(500)
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
|
||||||
|
INSERT dbo.StoryIntelligenceRuns
|
||||||
|
(
|
||||||
|
UserID, ProjectID, BookID, ChapterID, ChapterNumber, Status, SourceType, SourceLabel, SourceText,
|
||||||
|
SourceWordCount, SourceCharacterCount, SourceChapterCount, Model,
|
||||||
|
PromptVersion, PromptVersionsSummary, StartedUtc, CurrentStage, CurrentMessage,
|
||||||
|
CompletedScenes, FailedScenes
|
||||||
|
)
|
||||||
|
VALUES
|
||||||
|
(
|
||||||
|
@UserID, @ProjectID, @BookID, @ChapterID, @ChapterNumber, N'Pending', @SourceType, @SourceLabel, @SourceText,
|
||||||
|
@SourceWordCount, @SourceCharacterCount, @SourceChapterCount, @Model,
|
||||||
|
@PromptVersionsSummary, @PromptVersionsSummary, SYSUTCDATETIME(), N'Pending',
|
||||||
|
N'Queued for Story Intelligence processing.', 0, 0
|
||||||
|
);
|
||||||
|
|
||||||
|
SELECT CAST(SCOPE_IDENTITY() AS int) AS StoryIntelligenceRunID;
|
||||||
|
END;
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE OR ALTER PROCEDURE dbo.StoryIntelligenceRun_ClaimNextPending
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
SET XACT_ABORT ON;
|
||||||
|
|
||||||
|
DECLARE @StoryIntelligenceRunID int;
|
||||||
|
|
||||||
|
SELECT TOP (1) @StoryIntelligenceRunID = StoryIntelligenceRunID
|
||||||
|
FROM dbo.StoryIntelligenceRuns WITH (UPDLOCK, READPAST)
|
||||||
|
WHERE Status = N'Pending'
|
||||||
|
ORDER BY CreatedUtc;
|
||||||
|
|
||||||
|
IF @StoryIntelligenceRunID IS NULL
|
||||||
|
BEGIN
|
||||||
|
SELECT TOP (0)
|
||||||
|
StoryIntelligenceRunID, UserID, ProjectID, BookID, ChapterID, ChapterNumber, Status, SourceType, SourceLabel,
|
||||||
|
SourceText, SourceWordCount, SourceCharacterCount, SourceChapterCount, PromptVersion,
|
||||||
|
PromptVersionsSummary, Model, StartedUtc, CompletedUtc, FailureStage, TotalInputTokens,
|
||||||
|
TotalOutputTokens, TotalTokens, TotalDurationMs, EstimatedCostGBP, EstimatedCostUSD,
|
||||||
|
ErrorMessage, ErrorDetail, CurrentStage, CurrentMessage, TotalDetectedScenes,
|
||||||
|
CompletedScenes, FailedScenes, CancellationRequestedUtc, CancelledUtc, CreatedUtc, UpdatedUtc
|
||||||
|
FROM dbo.StoryIntelligenceRuns;
|
||||||
|
RETURN;
|
||||||
|
END;
|
||||||
|
|
||||||
|
UPDATE dbo.StoryIntelligenceRuns
|
||||||
|
SET Status = N'Running',
|
||||||
|
StartedUtc = SYSUTCDATETIME(),
|
||||||
|
CurrentStage = N'ChapterStructure',
|
||||||
|
CurrentMessage = N'Running Chapter Structure analysis.',
|
||||||
|
UpdatedUtc = SYSUTCDATETIME()
|
||||||
|
WHERE StoryIntelligenceRunID = @StoryIntelligenceRunID
|
||||||
|
AND Status = N'Pending';
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
StoryIntelligenceRunID, UserID, ProjectID, BookID, ChapterID, ChapterNumber, Status, SourceType, SourceLabel,
|
||||||
|
SourceText, SourceWordCount, SourceCharacterCount, SourceChapterCount, PromptVersion,
|
||||||
|
PromptVersionsSummary, Model, StartedUtc, CompletedUtc, FailureStage, TotalInputTokens,
|
||||||
|
TotalOutputTokens, TotalTokens, TotalDurationMs, EstimatedCostGBP, EstimatedCostUSD,
|
||||||
|
ErrorMessage, ErrorDetail, CurrentStage, CurrentMessage, TotalDetectedScenes,
|
||||||
|
CompletedScenes, FailedScenes, CancellationRequestedUtc, CancelledUtc, CreatedUtc, UpdatedUtc
|
||||||
|
FROM dbo.StoryIntelligenceRuns
|
||||||
|
WHERE StoryIntelligenceRunID = @StoryIntelligenceRunID;
|
||||||
|
END;
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE OR ALTER PROCEDURE dbo.StoryIntelligenceRun_ListAdmin
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
r.StoryIntelligenceRunID,
|
||||||
|
r.CreatedUtc,
|
||||||
|
r.UserID,
|
||||||
|
r.ProjectID,
|
||||||
|
p.ProjectName AS ProjectTitle,
|
||||||
|
r.BookID,
|
||||||
|
b.BookTitle,
|
||||||
|
r.ChapterID,
|
||||||
|
r.ChapterNumber,
|
||||||
|
r.Status,
|
||||||
|
r.SourceType,
|
||||||
|
r.FailureStage,
|
||||||
|
COUNT(sr.SceneResultID) AS SceneCount,
|
||||||
|
r.TotalTokens,
|
||||||
|
r.TotalDurationMs,
|
||||||
|
r.EstimatedCostGBP,
|
||||||
|
r.EstimatedCostUSD,
|
||||||
|
COALESCE(cr.ValidationErrorsCount, 0) + COALESCE(SUM(sr.ValidationErrorsCount), 0) AS ValidationErrorsCount,
|
||||||
|
COALESCE(cr.ValidationWarningsCount, 0) + COALESCE(SUM(sr.ValidationWarningsCount), 0) AS ValidationWarningsCount
|
||||||
|
FROM dbo.StoryIntelligenceRuns r
|
||||||
|
LEFT JOIN dbo.Projects p ON p.ProjectID = r.ProjectID
|
||||||
|
LEFT JOIN dbo.Books b ON b.BookID = r.BookID
|
||||||
|
LEFT JOIN dbo.StoryIntelligenceChapterResults cr ON cr.StoryIntelligenceRunID = r.StoryIntelligenceRunID
|
||||||
|
LEFT JOIN dbo.StoryIntelligenceSceneResults sr ON sr.StoryIntelligenceRunID = r.StoryIntelligenceRunID
|
||||||
|
GROUP BY
|
||||||
|
r.StoryIntelligenceRunID, r.CreatedUtc, r.UserID, r.ProjectID, p.ProjectName,
|
||||||
|
r.BookID, b.BookTitle, r.ChapterID, r.ChapterNumber, r.Status, r.SourceType,
|
||||||
|
r.FailureStage, r.TotalTokens, r.TotalDurationMs, r.EstimatedCostGBP, r.EstimatedCostUSD,
|
||||||
|
cr.ValidationErrorsCount, cr.ValidationWarningsCount
|
||||||
|
ORDER BY r.CreatedUtc DESC;
|
||||||
|
END;
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE OR ALTER PROCEDURE dbo.StoryIntelligenceRun_GetAdmin
|
||||||
|
@StoryIntelligenceRunID int
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
r.StoryIntelligenceRunID,
|
||||||
|
r.UserID,
|
||||||
|
r.ProjectID,
|
||||||
|
p.ProjectName AS ProjectTitle,
|
||||||
|
r.BookID,
|
||||||
|
b.BookTitle,
|
||||||
|
r.ChapterID,
|
||||||
|
r.ChapterNumber,
|
||||||
|
r.Status,
|
||||||
|
r.SourceType,
|
||||||
|
r.SourceFileName,
|
||||||
|
r.SourceFileSizeBytes,
|
||||||
|
r.SourceWordCount,
|
||||||
|
r.SourceCharacterCount,
|
||||||
|
r.SourceChapterCount,
|
||||||
|
r.SourceDetectedImagesCount,
|
||||||
|
r.SourceDetectedTablesCount,
|
||||||
|
r.SourceDetectedFootnotesCount,
|
||||||
|
r.SourceDetectedCommentsCount,
|
||||||
|
r.CurrentStage,
|
||||||
|
r.CurrentMessage,
|
||||||
|
r.TotalDetectedScenes,
|
||||||
|
r.CompletedScenes,
|
||||||
|
r.FailedScenes,
|
||||||
|
r.CancellationRequestedUtc,
|
||||||
|
r.CancelledUtc,
|
||||||
|
r.PromptVersion,
|
||||||
|
r.PromptVersionsSummary,
|
||||||
|
r.Model,
|
||||||
|
r.StartedUtc,
|
||||||
|
r.CompletedUtc,
|
||||||
|
r.FailureStage,
|
||||||
|
r.TotalInputTokens,
|
||||||
|
r.TotalOutputTokens,
|
||||||
|
r.TotalTokens,
|
||||||
|
r.TotalDurationMs,
|
||||||
|
r.EstimatedCostGBP,
|
||||||
|
r.EstimatedCostUSD,
|
||||||
|
r.ErrorMessage,
|
||||||
|
r.ErrorDetail,
|
||||||
|
r.CreatedUtc,
|
||||||
|
r.UpdatedUtc
|
||||||
|
FROM dbo.StoryIntelligenceRuns r
|
||||||
|
LEFT JOIN dbo.Projects p ON p.ProjectID = r.ProjectID
|
||||||
|
LEFT JOIN dbo.Books b ON b.BookID = r.BookID
|
||||||
|
WHERE r.StoryIntelligenceRunID = @StoryIntelligenceRunID;
|
||||||
|
END;
|
||||||
|
GO
|
||||||
@ -85,6 +85,7 @@
|
|||||||
<a class="btn btn-outline-primary btn-sm" asp-action="StoryIntelligenceTest">Dry run</a>
|
<a class="btn btn-outline-primary btn-sm" asp-action="StoryIntelligenceTest">Dry run</a>
|
||||||
<a class="btn btn-outline-primary btn-sm" asp-action="ChapterStructureTest">Chapter test</a>
|
<a class="btn btn-outline-primary btn-sm" asp-action="ChapterStructureTest">Chapter test</a>
|
||||||
<a class="btn btn-outline-primary btn-sm" asp-action="ChapterStoryIntelligenceTest">Pipeline test</a>
|
<a class="btn btn-outline-primary btn-sm" asp-action="ChapterStoryIntelligenceTest">Pipeline test</a>
|
||||||
|
<a class="btn btn-outline-primary btn-sm" asp-action="StoryIntelligenceExistingChapter">Queue chapter</a>
|
||||||
<a class="btn btn-outline-primary btn-sm" asp-action="StoryIntelligenceRuns">Saved runs</a>
|
<a class="btn btn-outline-primary btn-sm" asp-action="StoryIntelligenceRuns">Saved runs</a>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
|
|||||||
109
PlotLine/Views/Admin/StoryIntelligenceExistingChapter.cshtml
Normal file
109
PlotLine/Views/Admin/StoryIntelligenceExistingChapter.cshtml
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
@model StoryIntelligenceExistingChapterQueueViewModel
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = "Queue Story Intelligence From Chapter";
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="page-heading compact">
|
||||||
|
<div>
|
||||||
|
<p class="eyebrow">Admin utility</p>
|
||||||
|
<h1>Queue from existing chapter</h1>
|
||||||
|
<p class="lead-text">Run the persisted Story Intelligence pipeline from read-only PlotDirector chapter data.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav class="mb-3" aria-label="Breadcrumb">
|
||||||
|
<a asp-controller="Projects" asp-action="Index">Projects</a>
|
||||||
|
<span class="muted"> / </span>
|
||||||
|
<a asp-action="Index">Admin</a>
|
||||||
|
<span class="muted"> / Queue from existing chapter</span>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
@if (!string.IsNullOrWhiteSpace(Model.ErrorMessage))
|
||||||
|
{
|
||||||
|
<div class="alert alert-danger">@Model.ErrorMessage</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (!string.IsNullOrWhiteSpace(Model.WarningMessage))
|
||||||
|
{
|
||||||
|
<div class="alert alert-warning">@Model.WarningMessage</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<section class="edit-panel">
|
||||||
|
<form method="get" asp-action="StoryIntelligenceExistingChapter">
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label" for="ProjectID">Project</label>
|
||||||
|
<select class="form-select" id="ProjectID" name="ProjectID">
|
||||||
|
<option value="">Choose project</option>
|
||||||
|
@foreach (var project in Model.Projects)
|
||||||
|
{
|
||||||
|
<option value="@project.Value" selected="@(Model.Form.ProjectID == project.Value)">@project.Text</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label" for="BookID">Book</label>
|
||||||
|
<select class="form-select" id="BookID" name="BookID">
|
||||||
|
<option value="">Choose book</option>
|
||||||
|
@foreach (var book in Model.Books)
|
||||||
|
{
|
||||||
|
<option value="@book.Value" selected="@(Model.Form.BookID == book.Value)">@book.Text</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label" for="ChapterID">Chapter</label>
|
||||||
|
<select class="form-select" id="ChapterID" name="ChapterID">
|
||||||
|
<option value="">Choose chapter</option>
|
||||||
|
@foreach (var chapter in Model.Chapters)
|
||||||
|
{
|
||||||
|
<option value="@chapter.Value" selected="@(Model.Form.ChapterID == chapter.Value)">@chapter.Text</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="button-row mt-3">
|
||||||
|
<button type="submit" class="btn btn-outline-primary">Load selection</button>
|
||||||
|
<a class="btn btn-outline-secondary" asp-action="StoryIntelligenceRuns">Saved runs</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
@if (Model.SelectedChapter is not null)
|
||||||
|
{
|
||||||
|
<section class="edit-panel">
|
||||||
|
<h2>Selected source</h2>
|
||||||
|
<dl class="row mb-0">
|
||||||
|
<dt class="col-sm-3">Project</dt>
|
||||||
|
<dd class="col-sm-9">@Model.SelectedChapter.ProjectName</dd>
|
||||||
|
<dt class="col-sm-3">Book</dt>
|
||||||
|
<dd class="col-sm-9">@DisplayBook(Model.SelectedChapter)</dd>
|
||||||
|
<dt class="col-sm-3">Chapter</dt>
|
||||||
|
<dd class="col-sm-9">Chapter @Model.SelectedChapter.ChapterNumber: @Model.SelectedChapter.ChapterTitle</dd>
|
||||||
|
<dt class="col-sm-3">Word count</dt>
|
||||||
|
<dd class="col-sm-9">@DisplayCount(Model.SelectedChapter.SourceWordCount)</dd>
|
||||||
|
<dt class="col-sm-3">Character count</dt>
|
||||||
|
<dd class="col-sm-9">@DisplayCount(Model.SelectedChapter.SourceCharacterCount)</dd>
|
||||||
|
<dt class="col-sm-3">Text source</dt>
|
||||||
|
<dd class="col-sm-9">@(Model.SelectedChapter.HasSourceText ? "Stored chapter manuscript text is available." : "No stored chapter manuscript text is available.")</dd>
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
<form method="post" asp-action="QueueStoryIntelligenceExistingChapter" class="mt-3">
|
||||||
|
@Html.AntiForgeryToken()
|
||||||
|
<input type="hidden" name="ProjectID" value="@Model.Form.ProjectID" />
|
||||||
|
<input type="hidden" name="BookID" value="@Model.Form.BookID" />
|
||||||
|
<input type="hidden" name="ChapterID" value="@Model.Form.ChapterID" />
|
||||||
|
<button type="submit" class="btn btn-primary" disabled="@(!Model.SelectedChapter.HasSourceText)">Queue Story Intelligence run</button>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
}
|
||||||
|
|
||||||
|
@functions {
|
||||||
|
private static string DisplayCount(int? value)
|
||||||
|
=> value.HasValue ? value.Value.ToString("N0") : "Not available";
|
||||||
|
|
||||||
|
private static string DisplayBook(StoryIntelligenceExistingChapterSource source)
|
||||||
|
=> string.IsNullOrWhiteSpace(source.BookSubtitle)
|
||||||
|
? source.BookTitle
|
||||||
|
: $"{source.BookTitle}: {source.BookSubtitle}";
|
||||||
|
}
|
||||||
@ -104,6 +104,8 @@ else
|
|||||||
<dd>@DisplayName(run.ProjectTitle, run.ProjectID, "Project")</dd>
|
<dd>@DisplayName(run.ProjectTitle, run.ProjectID, "Project")</dd>
|
||||||
<dt>Book</dt>
|
<dt>Book</dt>
|
||||||
<dd>@DisplayName(run.BookTitle, run.BookID, "Book")</dd>
|
<dd>@DisplayName(run.BookTitle, run.BookID, "Book")</dd>
|
||||||
|
<dt>Chapter</dt>
|
||||||
|
<dd>@DisplayChapter(run.ChapterID, run.ChapterNumber)</dd>
|
||||||
<dt>Prompt version</dt>
|
<dt>Prompt version</dt>
|
||||||
<dd>@run.PromptVersion</dd>
|
<dd>@run.PromptVersion</dd>
|
||||||
<dt>Prompt versions summary</dt>
|
<dt>Prompt versions summary</dt>
|
||||||
@ -239,4 +241,11 @@ else
|
|||||||
: id.HasValue
|
: id.HasValue
|
||||||
? $"{label} {id.Value:N0}"
|
? $"{label} {id.Value:N0}"
|
||||||
: "None";
|
: "None";
|
||||||
|
|
||||||
|
private static string DisplayChapter(int? chapterId, decimal? chapterNumber)
|
||||||
|
=> chapterId.HasValue
|
||||||
|
? chapterNumber.HasValue
|
||||||
|
? $"Chapter {chapterNumber.Value:0.##} (ID {chapterId.Value:N0})"
|
||||||
|
: $"Chapter ID {chapterId.Value:N0}"
|
||||||
|
: "None";
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,6 +26,7 @@
|
|||||||
<section class="edit-panel">
|
<section class="edit-panel">
|
||||||
<div class="button-row mb-3">
|
<div class="button-row mb-3">
|
||||||
<a class="btn btn-primary" asp-action="ChapterStoryIntelligenceTest">Run pipeline test</a>
|
<a class="btn btn-primary" asp-action="ChapterStoryIntelligenceTest">Run pipeline test</a>
|
||||||
|
<a class="btn btn-outline-primary" asp-action="StoryIntelligenceExistingChapter">Queue from existing chapter</a>
|
||||||
<a class="btn btn-outline-secondary" asp-action="StoryIntelligenceDiagnostics">Diagnostics</a>
|
<a class="btn btn-outline-secondary" asp-action="StoryIntelligenceDiagnostics">Diagnostics</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user