using PlotLine.Data; using PlotLine.Models; namespace PlotLine.Services; public interface IStoryIntelligenceExistingChapterQueueService { Task BuildViewModelAsync(StoryIntelligenceExistingChapterQueueForm form); Task QueueAsync(int userId, StoryIntelligenceExistingChapterQueueForm form); } public sealed class StoryIntelligenceExistingChapterQueueService( IStoryIntelligenceSourceRepository sources, IStoryIntelligenceResultRepository runs, IStoryIntelligenceClient client, ILogger 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 BuildViewModelAsync(StoryIntelligenceExistingChapterQueueForm form) { var selected = await GetSelectedChapterAsync(form); return await BuildViewModelAsync(form, selected, warningMessage: BuildWarning(selected), errorMessage: null, queuedRunId: null); } public async Task 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, SourceParagraphCount = StoryIntelligenceParagraphs.Split(sourceText).Count, SourceChapterCount = 1, Model = StoryIntelligenceOptions.BuildModelSummary( clientStatus.ChapterStructureModel, clientStatus.SceneIntelligenceModel), 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 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 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; }