< Summary

Information
Class: Chronicis.Api.Services.PromptService
Assembly: Chronicis.Api
File(s): /home/runner/work/chronicis/chronicis/src/Chronicis.Api/Services/PromptService.cs
Line coverage
100%
Covered lines: 180
Uncovered lines: 0
Coverable lines: 180
Total lines: 283
Line coverage: 100%
Branch coverage
100%
Covered branches: 38
Total branches: 38
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
GeneratePrompts(...)100%11100%
AddMissingFundamentalPrompts(...)100%1010100%
AddStaleContentPrompts(...)100%44100%
AddSessionFollowUpPrompts(...)100%1212100%
AddEngagementPrompts(...)100%66100%
AddGeneralTips(...)100%66100%

File(s)

/home/runner/work/chronicis/chronicis/src/Chronicis.Api/Services/PromptService.cs

#LineLine coverage
 1using Chronicis.Shared.DTOs;
 2
 3namespace Chronicis.Api.Services;
 4
 5/// <summary>
 6/// Service for generating contextual prompts based on user state.
 7/// </summary>
 8public sealed class PromptService : IPromptService
 9{
 10    private readonly ILogger<PromptService> _logger;
 11
 12    // Configuration constants
 13    private const int StaleCharacterDays = 14;
 14    private const int SessionFollowUpDays = 7;
 15    private const int MaxPrompts = 3;
 16
 17    public PromptService(ILogger<PromptService> logger)
 18    {
 1719        _logger = logger;
 1720    }
 21
 22    public List<PromptDto> GeneratePrompts(DashboardDto dashboard)
 23    {
 1624        var prompts = new List<PromptDto>();
 25
 26        // Priority 1: Missing fundamentals
 1627        AddMissingFundamentalPrompts(dashboard, prompts);
 28
 29        // Priority 2: Stale content needing attention
 1630        AddStaleContentPrompts(dashboard, prompts);
 31
 32        // Priority 3: Session follow-up
 1633        AddSessionFollowUpPrompts(dashboard, prompts);
 34
 35        // Priority 4: Engagement suggestions
 1636        AddEngagementPrompts(dashboard, prompts);
 37
 38        // Priority 5: General tips (fallback)
 1639        AddGeneralTips(dashboard, prompts);
 40
 41        // Return top N prompts by priority
 1642        return prompts
 1643            .OrderBy(p => p.Priority)
 1644            .Take(MaxPrompts)
 1645            .ToList();
 46    }
 47
 48    private void AddMissingFundamentalPrompts(DashboardDto dashboard, List<PromptDto> prompts)
 49    {
 50        // No worlds at all
 1651        if (!dashboard.Worlds.Any())
 52        {
 153            prompts.Add(new PromptDto
 154            {
 155                Key = "no-worlds",
 156                Title = "Create Your First World",
 157                Message = "Every great campaign starts with a world. Create one to begin organizing your adventures.",
 158                Icon = "🌍",
 159                ActionText = "Create World",
 160                ActionUrl = "/dashboard", // Dashboard has the create button
 161                Priority = 10,
 162                Category = PromptCategory.MissingFundamental
 163            });
 164            return; // Don't add more prompts if no worlds
 65        }
 66
 67        // Has world but no campaigns
 1568        var worldsWithoutCampaigns = dashboard.Worlds.Where(w => !w.Campaigns.Any()).ToList();
 1569        if (worldsWithoutCampaigns.Any())
 70        {
 271            var world = worldsWithoutCampaigns.First();
 272            prompts.Add(new PromptDto
 273            {
 274                Key = $"no-campaign-{world.Id}",
 275                Title = "Start a Campaign",
 276                Message = $"Your world \"{world.Name}\" doesn't have any campaigns yet. Create one to track your adventu
 277                Icon = "📜",
 278                ActionText = "View World",
 279                ActionUrl = $"/world/{world.Slug}",
 280                Priority = 20,
 281                Category = PromptCategory.MissingFundamental
 282            });
 83        }
 84
 85        // Has campaigns but no claimed characters
 1586        if (!dashboard.ClaimedCharacters.Any() && dashboard.Worlds.Any(w => w.Campaigns.Any()))
 87        {
 388            prompts.Add(new PromptDto
 389            {
 390                Key = "no-characters",
 391                Title = "Claim Your Character",
 392                Message = "You haven't claimed any characters yet. Find your character in the tree and click 'Claim as M
 393                Icon = "👤",
 394                Priority = 30,
 395                Category = PromptCategory.MissingFundamental
 396            });
 97        }
 98
 99        // Has campaign but no sessions
 15100        var campaignsWithoutSessions = dashboard.Worlds
 15101            .SelectMany(w => w.Campaigns)
 15102            .Where(c => c.SessionCount == 0)
 15103            .ToList();
 104
 15105        if (campaignsWithoutSessions.Any())
 106        {
 2107            var campaign = campaignsWithoutSessions.First();
 2108            var world = dashboard.Worlds.First(w => w.Campaigns.Contains(campaign));
 2109            prompts.Add(new PromptDto
 2110            {
 2111                Key = $"no-sessions-{campaign.Id}",
 2112                Title = "Record Your First Session",
 2113                Message = $"Campaign \"{campaign.Name}\" has no session notes yet. Start documenting your adventures!",
 2114                Icon = "📝",
 2115                ActionText = "Add Session",
 2116                ActionUrl = $"/world/{world.Slug}",
 2117                Priority = 40,
 2118                Category = PromptCategory.MissingFundamental
 2119            });
 120        }
 15121    }
 122
 123    private void AddStaleContentPrompts(DashboardDto dashboard, List<PromptDto> prompts)
 124    {
 16125        var staleThreshold = DateTime.UtcNow.AddDays(-StaleCharacterDays);
 126
 127        // Stale characters (not updated in 14+ days)
 16128        var staleCharacters = dashboard.ClaimedCharacters
 16129            .Where(c => (c.ModifiedAt ?? c.CreatedAt) < staleThreshold)
 16130            .OrderBy(c => c.ModifiedAt ?? c.CreatedAt)
 16131            .ToList();
 132
 16133        if (staleCharacters.Any())
 134        {
 2135            var stalest = staleCharacters.First();
 2136            var daysSinceUpdate = (int)(DateTime.UtcNow - (stalest.ModifiedAt ?? stalest.CreatedAt)).TotalDays;
 137
 2138            prompts.Add(new PromptDto
 2139            {
 2140                Key = $"stale-character-{stalest.Id}",
 2141                Title = "Update Your Character",
 2142                Message = $"\"{stalest.Title}\" hasn't been updated in {daysSinceUpdate} days. How have they grown since
 2143                Icon = "✨",
 2144                ActionText = "View Character",
 2145                ActionUrl = $"/article/{stalest.Id}",
 2146                Priority = 50,
 2147                Category = PromptCategory.NeedsAttention
 2148            });
 149        }
 16150    }
 151
 152    private void AddSessionFollowUpPrompts(DashboardDto dashboard, List<PromptDto> prompts)
 153    {
 16154        var followUpThreshold = DateTime.UtcNow.AddDays(-SessionFollowUpDays);
 155
 156        // Find campaigns with recent sessions that might need follow-up notes
 62157        foreach (var world in dashboard.Worlds)
 158        {
 39159            foreach (var campaign in world.Campaigns.Where(c => c.IsActive))
 160            {
 5161                var latestSessionDate = campaign.CurrentArc?.LatestSessionDate;
 5162                if (latestSessionDate.HasValue)
 163                {
 3164                    var lastSession = latestSessionDate.Value;
 3165                    var daysSinceSession = (int)(DateTime.UtcNow - lastSession).TotalDays;
 166
 167                    // Session was 2-7 days ago - good time to add notes while fresh
 3168                    if (daysSinceSession >= 2 && daysSinceSession <= SessionFollowUpDays)
 169                    {
 1170                        prompts.Add(new PromptDto
 1171                        {
 1172                            Key = $"session-followup-{campaign.Id}",
 1173                            Title = "Add Session Notes",
 1174                            Message = $"Your last session in \"{campaign.Name}\" was {daysSinceSession} days ago. Add no
 1175                            Icon = "📖",
 1176                            ActionText = "View Campaign",
 1177                            ActionUrl = $"/world/{world.Slug}",
 1178                            Priority = 60,
 1179                            Category = PromptCategory.NeedsAttention
 1180                        });
 1181                        break; // Only one session follow-up prompt
 182                    }
 183                }
 184            }
 185        }
 16186    }
 187
 188    private void AddEngagementPrompts(DashboardDto dashboard, List<PromptDto> prompts)
 189    {
 190        // Only add engagement prompts if user has some content
 16191        if (!dashboard.Worlds.Any())
 1192            return;
 193
 15194        var totalArticles = dashboard.Worlds.Sum(w => w.ArticleCount);
 195
 196        // Suggest wiki links if they have multiple articles
 15197        if (totalArticles >= 5)
 198        {
 2199            prompts.Add(new PromptDto
 2200            {
 2201                Key = "try-wiki-links",
 2202                Title = "Connect Your Content",
 2203                Message = "Use [[Article Name]] in your notes to create links between articles. It's a great way to buil
 2204                Icon = "🔗",
 2205                Priority = 70,
 2206                Category = PromptCategory.Suggestion
 2207            });
 208        }
 209
 210        // Suggest AI summaries if they have backlinks
 15211        if (totalArticles >= 10)
 212        {
 1213            prompts.Add(new PromptDto
 1214            {
 1215                Key = "try-ai-summaries",
 1216                Title = "Try AI Summaries",
 1217                Message = "Articles with backlinks can generate AI summaries. Open an article and click 'Generate Summar
 1218                Icon = "🤖",
 1219                Priority = 75,
 1220                Category = PromptCategory.Suggestion
 1221            });
 222        }
 15223    }
 224
 225    private void AddGeneralTips(DashboardDto dashboard, List<PromptDto> prompts)
 226    {
 227        // Only add tips if we don't have enough higher-priority prompts
 17228        if (prompts.Count >= MaxPrompts)
 1229            return;
 230
 16231        var tips = new List<PromptDto>
 16232        {
 16233            new()
 16234            {
 16235                Key = "tip-keyboard-shortcuts",
 16236                Title = "Keyboard Shortcuts",
 16237                Message = "Press Ctrl+S to save, Ctrl+N to create a new article. Your work auto-saves as you type!",
 16238                Icon = "⌨️",
 16239                Priority = 100,
 16240                Category = PromptCategory.Tip
 16241            },
 16242            new()
 16243            {
 16244                Key = "tip-tree-search",
 16245                Title = "Quick Navigation",
 16246                Message = "Use the search box in the sidebar to quickly filter articles by title.",
 16247                Icon = "🔍",
 16248                Priority = 101,
 16249                Category = PromptCategory.Tip
 16250            },
 16251            new()
 16252            {
 16253                Key = "tip-drag-drop",
 16254                Title = "Organize with Drag & Drop",
 16255                Message = "Drag articles in the tree to reorganize your content hierarchy.",
 16256                Icon = "📂",
 16257                Priority = 102,
 16258                Category = PromptCategory.Tip
 16259            },
 16260            new()
 16261            {
 16262                Key = "tip-backlinks",
 16263                Title = "Discover Connections",
 16264                Message = "Click the metadata button on any article to see what other articles link to it.",
 16265                Icon = "🔙",
 16266                Priority = 103,
 16267                Category = PromptCategory.Tip
 16268            }
 16269        };
 270
 271        // Add a random tip if we need more prompts
 16272        var random = new Random();
 16273        var shuffledTips = tips.OrderBy(_ => random.Next()).ToList();
 274
 160275        foreach (var tip in shuffledTips)
 276        {
 64277            if (prompts.Count < MaxPrompts)
 278            {
 34279                prompts.Add(tip);
 280            }
 281        }
 16282    }
 283}