PlotDirector/PlotLine/Services/StoryIntelligenceExistingChapterQueueService.cs

146 lines
6.2 KiB
C#

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;
}