namespace PlotLine.Models; public static class StoryIntelligenceJobStatuses { public const string Pending = "Pending"; public const string Preparing = "Preparing"; public const string ReadingScenes = "ReadingScenes"; public const string AnalysingCharacters = "AnalysingCharacters"; public const string AnalysingRelationships = "AnalysingRelationships"; public const string AnalysingLocations = "AnalysingLocations"; public const string AnalysingAssets = "AnalysingAssets"; public const string AnalysingTimeline = "AnalysingTimeline"; public const string Finalising = "Finalising"; public const string Completed = "Completed"; public const string Cancelled = "Cancelled"; public const string Failed = "Failed"; } public sealed class StoryIntelligenceJob { public Guid JobID { get; init; } public int ConsentID { get; init; } public int UserID { get; init; } public int ProjectID { get; init; } public int BookID { get; init; } public string Status { get; init; } = StoryIntelligenceJobStatuses.Pending; public DateTime CreatedUtc { get; init; } public DateTime? StartedUtc { get; init; } public DateTime? CompletedUtc { get; init; } public DateTime? CancelledUtc { get; init; } public int ProgressPercent { get; init; } public string CurrentStage { get; init; } = StoryIntelligenceJobStatuses.Pending; public string CurrentMessage { get; init; } = "Story Intelligence is queued."; public string? EstimatedRemaining { get; init; } public string? ErrorMessage { get; init; } public DateTime UpdatedUtc { get; init; } public bool IsActive => Status is StoryIntelligenceJobStatuses.Pending or StoryIntelligenceJobStatuses.Preparing or StoryIntelligenceJobStatuses.ReadingScenes or StoryIntelligenceJobStatuses.AnalysingCharacters or StoryIntelligenceJobStatuses.AnalysingRelationships or StoryIntelligenceJobStatuses.AnalysingLocations or StoryIntelligenceJobStatuses.AnalysingAssets or StoryIntelligenceJobStatuses.AnalysingTimeline or StoryIntelligenceJobStatuses.Finalising; public bool IsCompleted => string.Equals(Status, StoryIntelligenceJobStatuses.Completed, StringComparison.Ordinal); public bool IsCancelled => string.Equals(Status, StoryIntelligenceJobStatuses.Cancelled, StringComparison.Ordinal); } public sealed class StoryIntelligenceJobProgress { public Guid JobID { get; init; } public int UserID { get; init; } public int ProjectID { get; init; } public int BookID { get; init; } public string Status { get; init; } = StoryIntelligenceJobStatuses.Pending; public string CurrentStage { get; init; } = StoryIntelligenceJobStatuses.Pending; public string CurrentMessage { get; init; } = "Story Intelligence is queued."; public int ProgressPercent { get; init; } public string? EstimatedRemaining { get; init; } public string ElapsedTime { get; init; } = "0 min"; public string? ErrorMessage { get; init; } public DateTime UpdatedUtc { get; init; } public bool IsActive { get; init; } public bool IsCompleted { get; init; } } public sealed class StoryIntelligenceRunProgressEvent { public int RunID { get; init; } 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; } = StoryIntelligenceRunStatuses.Pending; public string CurrentStage { get; init; } = string.Empty; public string CurrentMessage { get; init; } = string.Empty; public string EventType { get; init; } = string.Empty; public int? TotalDetectedScenes { get; init; } public int? CompletedScenes { get; init; } public int? FailedScenes { get; init; } public int? TotalTokens { get; init; } public long? TotalDurationMs { get; init; } public int? CurrentSceneNumber { get; init; } public string ElapsedTime { get; init; } = "0 sec"; public string? EstimatedRemaining { get; init; } public string? LatestSceneSummary { get; init; } public IReadOnlyList RecentDiscoveries { get; init; } = []; public string? ErrorMessage { get; init; } public DateTime UpdatedUtc { get; init; } = DateTime.UtcNow; public bool IsActive { get; init; } public bool IsCompleted { get; init; } } public sealed class StoryIntelligenceOptions { public string ApiKey { get; init; } = string.Empty; public string Model { get; init; } = string.Empty; public string ChapterStructureModel { get; init; } = string.Empty; public string SceneIntelligenceModel { get; init; } = string.Empty; public int MaxOutputTokens { get; init; } = 4000; public int? SceneIntelligenceMaxOutputTokens { get; init; } public int TimeoutSeconds { get; init; } = 120; public string EffectiveModel => FirstConfigured(Model); public string EffectiveChapterStructureModel => FirstConfigured(ChapterStructureModel, Model); public string EffectiveSceneIntelligenceModel => FirstConfigured(SceneIntelligenceModel, Model); public static string BuildModelSummary(string? chapterStructureModel, string? sceneIntelligenceModel) => $"Chapter={FirstConfigured(chapterStructureModel)}; Scene={FirstConfigured(sceneIntelligenceModel)}"; public static StoryIntelligenceStageModels ParseModelSummary( string? modelSummary, string defaultChapterStructureModel, string defaultSceneIntelligenceModel) { var chapterModel = defaultChapterStructureModel; var sceneModel = defaultSceneIntelligenceModel; if (!string.IsNullOrWhiteSpace(modelSummary)) { var segments = modelSummary.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); var matchedStage = false; foreach (var segment in segments) { var parts = segment.Split('=', 2, StringSplitOptions.TrimEntries); if (parts.Length != 2 || string.IsNullOrWhiteSpace(parts[1])) { continue; } if (string.Equals(parts[0], "Chapter", StringComparison.OrdinalIgnoreCase)) { chapterModel = parts[1]; matchedStage = true; } else if (string.Equals(parts[0], "Scene", StringComparison.OrdinalIgnoreCase)) { sceneModel = parts[1]; matchedStage = true; } } if (!matchedStage && !string.IsNullOrWhiteSpace(modelSummary)) { chapterModel = modelSummary.Trim(); sceneModel = modelSummary.Trim(); } } return new StoryIntelligenceStageModels( FirstConfigured(chapterModel, defaultChapterStructureModel), FirstConfigured(sceneModel, defaultSceneIntelligenceModel)); } private static string FirstConfigured(params string?[] values) => values.FirstOrDefault(value => !string.IsNullOrWhiteSpace(value))?.Trim() ?? string.Empty; } public sealed record StoryIntelligenceStageModels(string ChapterStructureModel, string SceneIntelligenceModel); public sealed class StoryIntelligencePricingOptions { public decimal? InputPerMillionUsd { get; init; } public decimal? OutputPerMillionUsd { get; init; } public decimal? UsdToGbpRate { get; init; } public Dictionary Models { get; init; } = new(StringComparer.OrdinalIgnoreCase); public StoryIntelligenceModelPricingOptions? GetPricingForModel(string? model) { if (!string.IsNullOrWhiteSpace(model) && Models.TryGetValue(model.Trim(), out var modelPricing) && (modelPricing.InputPerMillionUsd.HasValue || modelPricing.OutputPerMillionUsd.HasValue)) { return modelPricing; } return InputPerMillionUsd.HasValue || OutputPerMillionUsd.HasValue ? new StoryIntelligenceModelPricingOptions { InputPerMillionUsd = InputPerMillionUsd, OutputPerMillionUsd = OutputPerMillionUsd } : null; } } public sealed class StoryIntelligenceModelPricingOptions { public decimal? InputPerMillionUsd { get; init; } public decimal? OutputPerMillionUsd { get; init; } } public sealed class StoryIntelligenceRun { public Guid RunId { get; init; } = Guid.NewGuid(); public int ProjectId { get; init; } public int BookId { get; init; } public DateTime StartedUtc { get; init; } = DateTime.UtcNow; public DateTime? CompletedUtc { get; init; } public string Status { get; init; } = "Pending"; public string PromptVersion { get; init; } = string.Empty; public string Model { get; init; } = string.Empty; public int? InputTokens { get; init; } public int? OutputTokens { get; init; } public decimal? EstimatedCost { get; init; } public bool Cancelled { get; init; } public string? ErrorMessage { get; init; } } public sealed class StoryIntelligenceClientResult { public string RawResponseText { get; init; } = string.Empty; public string Model { get; init; } = string.Empty; public TimeSpan Duration { get; init; } public int RetryCount { get; init; } public int? InputTokens { get; init; } public int? OutputTokens { get; init; } public decimal? EstimatedCost { get; init; } }