< 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
0%
Covered lines: 0
Uncovered lines: 180
Coverable lines: 180
Total lines: 282
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 42
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%210%
GeneratePrompts(...)100%210%
AddMissingFundamentalPrompts(...)0%110100%
AddStaleContentPrompts(...)0%7280%
AddSessionFollowUpPrompts(...)0%156120%
AddEngagementPrompts(...)0%4260%
AddGeneralTips(...)0%4260%

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 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
 017    public PromptService(ILogger<PromptService> logger)
 18    {
 019        _logger = logger;
 020    }
 21
 22    public List<PromptDto> GeneratePrompts(DashboardDto dashboard)
 23    {
 024        var prompts = new List<PromptDto>();
 25
 26        // Priority 1: Missing fundamentals
 027        AddMissingFundamentalPrompts(dashboard, prompts);
 28
 29        // Priority 2: Stale content needing attention
 030        AddStaleContentPrompts(dashboard, prompts);
 31
 32        // Priority 3: Session follow-up
 033        AddSessionFollowUpPrompts(dashboard, prompts);
 34
 35        // Priority 4: Engagement suggestions
 036        AddEngagementPrompts(dashboard, prompts);
 37
 38        // Priority 5: General tips (fallback)
 039        AddGeneralTips(dashboard, prompts);
 40
 41        // Return top N prompts by priority
 042        return prompts
 043            .OrderBy(p => p.Priority)
 044            .Take(MaxPrompts)
 045            .ToList();
 46    }
 47
 48    private void AddMissingFundamentalPrompts(DashboardDto dashboard, List<PromptDto> prompts)
 49    {
 50        // No worlds at all
 051        if (!dashboard.Worlds.Any())
 52        {
 053            prompts.Add(new PromptDto
 054            {
 055                Key = "no-worlds",
 056                Title = "Create Your First World",
 057                Message = "Every great campaign starts with a world. Create one to begin organizing your adventures.",
 058                Icon = "🌍",
 059                ActionText = "Create World",
 060                ActionUrl = "/dashboard", // Dashboard has the create button
 061                Priority = 10,
 062                Category = PromptCategory.MissingFundamental
 063            });
 064            return; // Don't add more prompts if no worlds
 65        }
 66
 67        // Has world but no campaigns
 068        var worldsWithoutCampaigns = dashboard.Worlds.Where(w => !w.Campaigns.Any()).ToList();
 069        if (worldsWithoutCampaigns.Any())
 70        {
 071            var world = worldsWithoutCampaigns.First();
 072            prompts.Add(new PromptDto
 073            {
 074                Key = $"no-campaign-{world.Id}",
 075                Title = "Start a Campaign",
 076                Message = $"Your world \"{world.Name}\" doesn't have any campaigns yet. Create one to track your adventu
 077                Icon = "📜",
 078                ActionText = "View World",
 079                ActionUrl = $"/world/{world.Slug}",
 080                Priority = 20,
 081                Category = PromptCategory.MissingFundamental
 082            });
 83        }
 84
 85        // Has campaigns but no claimed characters
 086        if (!dashboard.ClaimedCharacters.Any() && dashboard.Worlds.Any(w => w.Campaigns.Any()))
 87        {
 088            prompts.Add(new PromptDto
 089            {
 090                Key = "no-characters",
 091                Title = "Claim Your Character",
 092                Message = "You haven't claimed any characters yet. Find your character in the tree and click 'Claim as M
 093                Icon = "👤",
 094                Priority = 30,
 095                Category = PromptCategory.MissingFundamental
 096            });
 97        }
 98
 99        // Has campaign but no sessions
 0100        var campaignsWithoutSessions = dashboard.Worlds
 0101            .SelectMany(w => w.Campaigns)
 0102            .Where(c => c.SessionCount == 0)
 0103            .ToList();
 104
 0105        if (campaignsWithoutSessions.Any())
 106        {
 0107            var campaign = campaignsWithoutSessions.First();
 0108            var world = dashboard.Worlds.First(w => w.Campaigns.Contains(campaign));
 0109            prompts.Add(new PromptDto
 0110            {
 0111                Key = $"no-sessions-{campaign.Id}",
 0112                Title = "Record Your First Session",
 0113                Message = $"Campaign \"{campaign.Name}\" has no session notes yet. Start documenting your adventures!",
 0114                Icon = "📝",
 0115                ActionText = "Add Session",
 0116                ActionUrl = $"/world/{world.Slug}",
 0117                Priority = 40,
 0118                Category = PromptCategory.MissingFundamental
 0119            });
 120        }
 0121    }
 122
 123    private void AddStaleContentPrompts(DashboardDto dashboard, List<PromptDto> prompts)
 124    {
 0125        var staleThreshold = DateTime.UtcNow.AddDays(-StaleCharacterDays);
 126
 127        // Stale characters (not updated in 14+ days)
 0128        var staleCharacters = dashboard.ClaimedCharacters
 0129            .Where(c => (c.ModifiedAt ?? c.CreatedAt) < staleThreshold)
 0130            .OrderBy(c => c.ModifiedAt ?? c.CreatedAt)
 0131            .ToList();
 132
 0133        if (staleCharacters.Any())
 134        {
 0135            var stalest = staleCharacters.First();
 0136            var daysSinceUpdate = (int)(DateTime.UtcNow - (stalest.ModifiedAt ?? stalest.CreatedAt)).TotalDays;
 137
 0138            prompts.Add(new PromptDto
 0139            {
 0140                Key = $"stale-character-{stalest.Id}",
 0141                Title = "Update Your Character",
 0142                Message = $"\"{stalest.Title}\" hasn't been updated in {daysSinceUpdate} days. How have they grown since
 0143                Icon = "✨",
 0144                ActionText = "View Character",
 0145                ActionUrl = $"/article/{stalest.Id}",
 0146                Priority = 50,
 0147                Category = PromptCategory.NeedsAttention
 0148            });
 149        }
 0150    }
 151
 152    private void AddSessionFollowUpPrompts(DashboardDto dashboard, List<PromptDto> prompts)
 153    {
 0154        var followUpThreshold = DateTime.UtcNow.AddDays(-SessionFollowUpDays);
 155
 156        // Find campaigns with recent sessions that might need follow-up notes
 0157        foreach (var world in dashboard.Worlds)
 158        {
 0159            foreach (var campaign in world.Campaigns.Where(c => c.IsActive))
 160            {
 0161                if (campaign.CurrentArc?.LatestSessionDate != null)
 162                {
 0163                    var lastSession = campaign.CurrentArc.LatestSessionDate.Value;
 0164                    var daysSinceSession = (int)(DateTime.UtcNow - lastSession).TotalDays;
 165
 166                    // Session was 2-7 days ago - good time to add notes while fresh
 0167                    if (daysSinceSession >= 2 && daysSinceSession <= SessionFollowUpDays)
 168                    {
 0169                        prompts.Add(new PromptDto
 0170                        {
 0171                            Key = $"session-followup-{campaign.Id}",
 0172                            Title = "Add Session Notes",
 0173                            Message = $"Your last session in \"{campaign.Name}\" was {daysSinceSession} days ago. Add no
 0174                            Icon = "📖",
 0175                            ActionText = "View Campaign",
 0176                            ActionUrl = $"/world/{world.Slug}",
 0177                            Priority = 60,
 0178                            Category = PromptCategory.NeedsAttention
 0179                        });
 0180                        break; // Only one session follow-up prompt
 181                    }
 182                }
 183            }
 184        }
 0185    }
 186
 187    private void AddEngagementPrompts(DashboardDto dashboard, List<PromptDto> prompts)
 188    {
 189        // Only add engagement prompts if user has some content
 0190        if (!dashboard.Worlds.Any())
 0191            return;
 192
 0193        var totalArticles = dashboard.Worlds.Sum(w => w.ArticleCount);
 194
 195        // Suggest wiki links if they have multiple articles
 0196        if (totalArticles >= 5)
 197        {
 0198            prompts.Add(new PromptDto
 0199            {
 0200                Key = "try-wiki-links",
 0201                Title = "Connect Your Content",
 0202                Message = "Use [[Article Name]] in your notes to create links between articles. It's a great way to buil
 0203                Icon = "🔗",
 0204                Priority = 70,
 0205                Category = PromptCategory.Suggestion
 0206            });
 207        }
 208
 209        // Suggest AI summaries if they have backlinks
 0210        if (totalArticles >= 10)
 211        {
 0212            prompts.Add(new PromptDto
 0213            {
 0214                Key = "try-ai-summaries",
 0215                Title = "Try AI Summaries",
 0216                Message = "Articles with backlinks can generate AI summaries. Open an article and click 'Generate Summar
 0217                Icon = "🤖",
 0218                Priority = 75,
 0219                Category = PromptCategory.Suggestion
 0220            });
 221        }
 0222    }
 223
 224    private void AddGeneralTips(DashboardDto dashboard, List<PromptDto> prompts)
 225    {
 226        // Only add tips if we don't have enough higher-priority prompts
 0227        if (prompts.Count >= MaxPrompts)
 0228            return;
 229
 0230        var tips = new List<PromptDto>
 0231        {
 0232            new()
 0233            {
 0234                Key = "tip-keyboard-shortcuts",
 0235                Title = "Keyboard Shortcuts",
 0236                Message = "Press Ctrl+S to save, Ctrl+N to create a new article. Your work auto-saves as you type!",
 0237                Icon = "⌨️",
 0238                Priority = 100,
 0239                Category = PromptCategory.Tip
 0240            },
 0241            new()
 0242            {
 0243                Key = "tip-tree-search",
 0244                Title = "Quick Navigation",
 0245                Message = "Use the search box in the sidebar to quickly filter articles by title.",
 0246                Icon = "🔍",
 0247                Priority = 101,
 0248                Category = PromptCategory.Tip
 0249            },
 0250            new()
 0251            {
 0252                Key = "tip-drag-drop",
 0253                Title = "Organize with Drag & Drop",
 0254                Message = "Drag articles in the tree to reorganize your content hierarchy.",
 0255                Icon = "📂",
 0256                Priority = 102,
 0257                Category = PromptCategory.Tip
 0258            },
 0259            new()
 0260            {
 0261                Key = "tip-backlinks",
 0262                Title = "Discover Connections",
 0263                Message = "Click the metadata button on any article to see what other articles link to it.",
 0264                Icon = "🔙",
 0265                Priority = 103,
 0266                Category = PromptCategory.Tip
 0267            }
 0268        };
 269
 270        // Add a random tip if we need more prompts
 0271        var random = new Random();
 0272        var shuffledTips = tips.OrderBy(_ => random.Next()).ToList();
 273
 0274        foreach (var tip in shuffledTips)
 275        {
 0276            if (prompts.Count < MaxPrompts)
 277            {
 0278                prompts.Add(tip);
 279            }
 280        }
 0281    }
 282}