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,
|
||||
IChapterStructureDryRunService chapterStructureDryRun,
|
||||
IChapterStoryIntelligenceDryRunService chapterStoryIntelligenceDryRun,
|
||||
IStoryIntelligenceResultPersistenceService storyIntelligenceResults) : Controller
|
||||
IStoryIntelligenceResultPersistenceService storyIntelligenceResults,
|
||||
IStoryIntelligenceExistingChapterQueueService existingChapterQueue) : Controller
|
||||
{
|
||||
[HttpGet("")]
|
||||
[HttpGet("index")]
|
||||
@ -107,6 +108,31 @@ public sealed class AdminController(
|
||||
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")]
|
||||
public async Task<IActionResult> StoryIntelligenceRuns()
|
||||
{
|
||||
|
||||
@ -163,6 +163,8 @@ public sealed class StoryIntelligenceResultRepository(ISqlConnectionFactory conn
|
||||
request.UserID,
|
||||
request.ProjectID,
|
||||
request.BookID,
|
||||
request.ChapterID,
|
||||
request.ChapterNumber,
|
||||
request.SourceType,
|
||||
request.SourceLabel,
|
||||
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? ProjectID { 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 SourceLabel { 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 int? BookID { 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 SourceType { get; init; } = string.Empty;
|
||||
public string? FailureStage { get; init; }
|
||||
@ -142,6 +146,8 @@ public sealed class StoryIntelligenceSavedRun
|
||||
public string? ProjectTitle { get; init; }
|
||||
public int? BookID { 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 SourceType { get; init; } = string.Empty;
|
||||
public string? SourceFileName { get; init; }
|
||||
@ -184,6 +190,8 @@ public sealed class StoryIntelligenceQueuedRun
|
||||
public int UserID { get; init; }
|
||||
public int? ProjectID { 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 SourceType { get; init; } = string.Empty;
|
||||
public string? SourceLabel { get; init; }
|
||||
@ -217,6 +225,48 @@ public sealed class StoryIntelligenceQueuedRun
|
||||
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 int ChapterResultID { get; init; }
|
||||
|
||||
@ -138,6 +138,7 @@ public class Program
|
||||
builder.Services.AddScoped<IOnboardingBuildRepository, OnboardingBuildRepository>();
|
||||
builder.Services.AddScoped<IStoryIntelligenceRepository, StoryIntelligenceRepository>();
|
||||
builder.Services.AddScoped<IStoryIntelligenceResultRepository, StoryIntelligenceResultRepository>();
|
||||
builder.Services.AddScoped<IStoryIntelligenceSourceRepository, StoryIntelligenceSourceRepository>();
|
||||
builder.Services.AddScoped<IProjectAccessRepository, ProjectAccessRepository>();
|
||||
builder.Services.AddScoped<IProjectCollaborationRepository, ProjectCollaborationRepository>();
|
||||
builder.Services.AddScoped<IProjectActivityRepository, ProjectActivityRepository>();
|
||||
@ -196,6 +197,7 @@ public class Program
|
||||
builder.Services.AddScoped<IChapterStructureDryRunService, ChapterStructureDryRunService>();
|
||||
builder.Services.AddScoped<IChapterStoryIntelligenceDryRunService, ChapterStoryIntelligenceDryRunService>();
|
||||
builder.Services.AddScoped<IStoryIntelligenceResultPersistenceService, StoryIntelligenceResultPersistenceService>();
|
||||
builder.Services.AddScoped<IStoryIntelligenceExistingChapterQueueService, StoryIntelligenceExistingChapterQueueService>();
|
||||
builder.Services.AddScoped<IPersistedStoryIntelligenceRunner, PersistedStoryIntelligenceRunner>();
|
||||
builder.Services.AddScoped<IProjectAccessService, ProjectAccessService>();
|
||||
builder.Services.AddScoped<ISubscriptionService, SubscriptionService>();
|
||||
|
||||
@ -95,8 +95,8 @@ public sealed class PersistedStoryIntelligenceRunner(
|
||||
{
|
||||
ProjectID = run.ProjectID,
|
||||
BookID = run.BookID,
|
||||
ChapterID = null,
|
||||
ChapterNumber = 1,
|
||||
ChapterID = run.ChapterID,
|
||||
ChapterNumber = ChapterNumber(run),
|
||||
SourceLabel = SourceLabel(run),
|
||||
PromptVersion = chapterPromptVersion,
|
||||
Model = chapterClientResult.Model,
|
||||
@ -172,7 +172,7 @@ public sealed class PersistedStoryIntelligenceRunner(
|
||||
{
|
||||
ProjectID = run.ProjectID,
|
||||
BookID = run.BookID,
|
||||
ChapterID = null,
|
||||
ChapterID = run.ChapterID,
|
||||
SceneID = null,
|
||||
TemporarySceneNumber = block.TemporarySceneNumber,
|
||||
StartParagraph = block.StartParagraph,
|
||||
@ -274,7 +274,7 @@ public sealed class PersistedStoryIntelligenceRunner(
|
||||
{
|
||||
ProjectID = run.ProjectID,
|
||||
BookID = run.BookID,
|
||||
ChapterID = null,
|
||||
ChapterID = run.ChapterID,
|
||||
SceneID = null,
|
||||
TemporarySceneNumber = block.TemporarySceneNumber,
|
||||
StartParagraph = block.StartParagraph,
|
||||
@ -336,8 +336,8 @@ public sealed class PersistedStoryIntelligenceRunner(
|
||||
{
|
||||
projectId = run.ProjectID,
|
||||
bookId = run.BookID,
|
||||
chapterId = (int?)null,
|
||||
chapterNumber = 1,
|
||||
chapterId = run.ChapterID,
|
||||
chapterNumber = ChapterNumber(run),
|
||||
sourceLabel = SourceLabel(run)
|
||||
}, JsonOptions);
|
||||
|
||||
@ -346,9 +346,9 @@ public sealed class PersistedStoryIntelligenceRunner(
|
||||
{
|
||||
projectId = run.ProjectID,
|
||||
bookId = run.BookID,
|
||||
chapterId = (int?)null,
|
||||
chapterId = run.ChapterID,
|
||||
sceneId = (int?)null,
|
||||
chapterNumber = 1,
|
||||
chapterNumber = ChapterNumber(run),
|
||||
sceneNumber = boundary.SceneNumber,
|
||||
sourceLabel = $"{SourceLabel(run)}, suggested scene {boundary.SceneNumber}"
|
||||
}, JsonOptions);
|
||||
@ -388,6 +388,9 @@ public sealed class PersistedStoryIntelligenceRunner(
|
||||
private static string SourceLabel(StoryIntelligenceQueuedRun run)
|
||||
=> 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)
|
||||
=> string.Join(
|
||||
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,
|
||||
ProjectID = form.ProjectID,
|
||||
BookID = form.BookID,
|
||||
ChapterID = form.ChapterID,
|
||||
ChapterNumber = form.ChapterNumber,
|
||||
SourceType = "AdminText",
|
||||
SourceLabel = CleanSourceLabel(form.SourceLabel),
|
||||
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="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="StoryIntelligenceExistingChapter">Queue chapter</a>
|
||||
<a class="btn btn-outline-primary btn-sm" asp-action="StoryIntelligenceRuns">Saved runs</a>
|
||||
</div>
|
||||
</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>
|
||||
<dt>Book</dt>
|
||||
<dd>@DisplayName(run.BookTitle, run.BookID, "Book")</dd>
|
||||
<dt>Chapter</dt>
|
||||
<dd>@DisplayChapter(run.ChapterID, run.ChapterNumber)</dd>
|
||||
<dt>Prompt version</dt>
|
||||
<dd>@run.PromptVersion</dd>
|
||||
<dt>Prompt versions summary</dt>
|
||||
@ -239,4 +241,11 @@ else
|
||||
: id.HasValue
|
||||
? $"{label} {id.Value:N0}"
|
||||
: "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">
|
||||
<div class="button-row mb-3">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user