< Summary

Information
Class: Chronicis.Client.Components.Shared.AISummarySection
Assembly: Chronicis.Client
File(s): /home/runner/work/chronicis/chronicis/src/Chronicis.Client/Components/Shared/AISummarySection.razor
Line coverage
0%
Covered lines: 0
Uncovered lines: 193
Coverable lines: 193
Total lines: 511
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 132
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

File(s)

/home/runner/work/chronicis/chronicis/src/Chronicis.Client/Components/Shared/AISummarySection.razor

#LineLine coverage
 1@using Chronicis.Shared.DTOs
 2@inject IAISummaryApiService SummaryApi
 3@inject ISnackbar Snackbar
 4@inject IJSRuntime JSRuntime
 5
 6<div class="ai-summary-section @(_summaryData?.HasSummary == true ? "has-summary" : "")">
 7    <div class="ai-summary-header" @onclick="ToggleExpanded">
 8        <div class="header-left">
 9            <MudIcon Icon="@Icons.Material.Filled.AutoAwesome"
 10                     Size="Size.Small"
 11                     Class="header-icon" />
 12            <MudText Typo="Typo.subtitle1" Class="header-title">AI Summary</MudText>
 013            @if (_summaryData?.HasSummary == true)
 14            {
 15                <MudChip T="string"
 16                         Size="Size.Small"
 17                         Variant="Variant.Outlined"
 18                         Class="timestamp-chip">
 019                    @GetRelativeTime(_summaryData.GeneratedAt!.Value)
 20                </MudChip>
 21            }
 22        </div>
 23        <MudIcon Icon="@(IsExpanded ? Icons.Material.Filled.ExpandLess : Icons.Material.Filled.ExpandMore)"
 24                 Size="Size.Small"
 25                 Class="expand-icon" />
 26    </div>
 27
 028    @if (IsExpanded)
 29    {
 30        <div class="ai-summary-content">
 031            @if (_isLoading)
 32            {
 33                <div class="loading-state">
 34                    <MudProgressLinear Indeterminate="true" Color="Color.Primary" Class="loading-bar" />
 35                    <div class="loading-text">
 36                        <MudIcon Icon="@Icons.Material.Filled.Psychology" Size="Size.Small" />
 037                        <MudText Typo="Typo.body2">@_loadingMessage</MudText>
 38                    </div>
 39                </div>
 40            }
 041            else if (_summaryData?.HasSummary == true)
 42            {
 43                @* Summary Display *@
 44                <div class="summary-display">
 045                    <div class="summary-text">@_summaryData.Summary</div>
 46
 47                    <div class="summary-footer">
 048                        @if (!string.IsNullOrEmpty(_summaryData.TemplateName))
 49                        {
 50                            <MudText Typo="Typo.caption" Class="template-badge">
 51                                <MudIcon Icon="@Icons.Material.Filled.Style" Size="Size.Small" />
 052                                @_summaryData.TemplateName
 53                            </MudText>
 54                        }
 55
 56                        <div class="summary-actions">
 57                            <MudTooltip Text="Regenerate summary">
 58                                <MudIconButton Icon="@Icons.Material.Filled.Refresh"
 59                                               Size="Size.Small"
 60                                               OnClick="RegenerateSummary" />
 61                            </MudTooltip>
 62                            <MudTooltip Text="Copy to clipboard">
 63                                <MudIconButton Icon="@Icons.Material.Filled.ContentCopy"
 64                                               Size="Size.Small"
 65                                               OnClick="CopySummary" />
 66                            </MudTooltip>
 67                            <MudTooltip Text="Clear summary">
 68                                <MudIconButton Icon="@Icons.Material.Filled.Delete"
 69                                               Size="Size.Small"
 70                                               Color="Color.Error"
 71                                               OnClick="ClearSummary" />
 72                            </MudTooltip>
 73                        </div>
 74                    </div>
 75                </div>
 76            }
 077            else if (_estimate != null)
 78            {
 79                @* Generate UI *@
 080                @if (_estimate.SourceCount == 0)
 81                {
 82                    <div class="no-sources">
 83                        <MudIcon Icon="@Icons.Material.Filled.Info" Color="Color.Info" />
 084                        <MudText Typo="Typo.body2">@NoSourcesMessage</MudText>
 85                    </div>
 86                }
 87                else
 88                {
 89                    <div class="generate-ui">
 90                        @* Main Controls Row *@
 91                        <div class="generate-main">
 92                            <MudSelect T="Guid?"
 93                                       @bind-Value="_selectedTemplateId"
 94                                       Label="Template"
 95                                       Variant="Variant.Outlined"
 96                                       Dense="true"
 97                                       Class="template-select">
 098                                @foreach (var template in _templates)
 99                                {
 100                                    <MudSelectItem T="Guid?" Value="@template.Id">
 0101                                        @template.Name
 102                                    </MudSelectItem>
 103                                }
 104                            </MudSelect>
 105
 106                            <MudButton Variant="Variant.Filled"
 107                                       Color="Color.Primary"
 108                                       StartIcon="@Icons.Material.Filled.AutoAwesome"
 109                                       OnClick="GenerateSummary"
 110                                       Disabled="_isLoading"
 111                                       Class="generate-button">
 112                                Generate
 113                            </MudButton>
 114                        </div>
 115
 116                        @* Source count hint *@
 117                        <MudText Typo="Typo.caption" Class="source-hint">
 0118                            @_estimate.SourceCount @SourceLabel@(_estimate.SourceCount == 1 ? "" : "s") will be analyzed
 119                        </MudText>
 120
 121                        @* Advanced Options (collapsed by default) *@
 122                        <MudExpansionPanels Class="advanced-panel" Elevation="0">
 123                            <MudExpansionPanel Text="Advanced Options" Dense="true" @bind-Expanded="_advancedExpanded">
 124                                <div class="advanced-content">
 125                                    <MudTextField T="string"
 126                                                  @bind-Value="_customPrompt"
 127                                                  Label="Custom Instructions"
 128                                                  Variant="Variant.Outlined"
 129                                                  Lines="2"
 130                                                  Placeholder="@CustomPromptPlaceholder"
 131                                                  HelperText="Add extra guidance for the AI (optional)"
 132                                                  Class="custom-prompt-field" />
 133
 134                                    <MudCheckBox T="bool"
 135                                                 @bind-Value="_saveConfiguration"
 136                                                 Label="@RememberSettingsLabel"
 137                                                 Dense="true"
 138                                                 Class="remember-checkbox" />
 139
 140                                    <MudTooltip Text="@GetEstimateTooltip()">
 141                                        <MudText Typo="Typo.caption" Class="cost-hint">
 142                                            <MudIcon Icon="@Icons.Material.Filled.Payments" Size="Size.Small" />
 0143                                            Est. ~$@_estimate.EstimatedCostUSD.ToString("F4")
 144                                        </MudText>
 145                                    </MudTooltip>
 146                                </div>
 147                            </MudExpansionPanel>
 148                        </MudExpansionPanels>
 149                    </div>
 150                }
 151            }
 0152            else if (!string.IsNullOrEmpty(_errorMessage))
 153            {
 154                <MudAlert Severity="Severity.Error" Dense="true" Class="error-alert">
 0155                    @_errorMessage
 156                </MudAlert>
 157            }
 158        </div>
 159    }
 160</div>
 161
 162@code {
 163    /// <summary>
 164    /// The entity ID (Article, Campaign, or Arc)
 165    /// </summary>
 166    [Parameter, EditorRequired]
 0167    public Guid EntityId { get; set; }
 168
 169    /// <summary>
 170    /// The type of entity: "Article", "Campaign", or "Arc"
 171    /// </summary>
 172    [Parameter]
 0173    public string EntityType { get; set; } = "Article";
 174
 175    [Parameter]
 0176    public bool IsExpanded { get; set; } = false;
 177
 178    [Parameter]
 0179    public EventCallback<bool> IsExpandedChanged { get; set; }
 180
 181    // Track previous parameter values to detect actual changes
 182    private Guid _previousEntityId;
 0183    private string _previousEntityType = string.Empty;
 184
 185    private ArticleSummaryDto? _articleSummaryData;
 186    private EntitySummaryDto? _entitySummaryData;
 187    private SummaryEstimateDto? _estimate;
 0188    private List<SummaryTemplateDto> _templates = new();
 189    private bool _isLoading = false;
 0190    private string _loadingMessage = "";
 0191    private string _errorMessage = "";
 192
 193    private Guid? _selectedTemplateId;
 194    private string? _customPrompt;
 195    private bool _saveConfiguration = false;
 196    private bool _advancedExpanded = false;
 197
 198    // Unified summary accessor
 0199    private SummaryDataAccessor? _summaryData => EntityType == "Article"
 0200        ? (_articleSummaryData != null ? new SummaryDataAccessor(_articleSummaryData) : null)
 0201        : (_entitySummaryData != null ? new SummaryDataAccessor(_entitySummaryData) : null);
 202
 0203    private string NoSourcesMessage => EntityType switch
 0204    {
 0205        "Article" => "This article has no content and isn't referenced by other articles. Add content or create wiki lin
 0206        "Campaign" => "No session notes found. Add sessions to your arcs to generate a campaign summary.",
 0207        "Arc" => "No session notes found in this arc yet.",
 0208        _ => "No sources available for summary generation."
 0209    };
 210
 0211    private string SourceLabel => EntityType == "Article" ? "source" : "session";
 212
 0213    private string RememberSettingsLabel => $"Remember settings for this {EntityType.ToLower()}";
 214
 0215    private string CustomPromptPlaceholder => EntityType switch
 0216    {
 0217        "Article" => "e.g., Focus on relationships and motivations...",
 0218        "Campaign" => "e.g., Emphasize major plot developments...",
 0219        "Arc" => "e.g., Highlight character growth in this arc...",
 0220        _ => "Additional instructions for the AI..."
 0221    };
 222
 223    protected override async Task OnInitializedAsync()
 224    {
 0225        _templates = await SummaryApi.GetTemplatesAsync();
 226
 227        // Set default template if none selected
 0228        if (_selectedTemplateId == null && _templates.Any())
 229        {
 0230            _selectedTemplateId = _templates.FirstOrDefault(t => t.Name == "Default")?.Id
 0231                                  ?? _templates.First().Id;
 232        }
 0233    }
 234
 235    protected override async Task OnParametersSetAsync()
 236    {
 237        // Only reload if EntityId or EntityType actually changed
 0238        var entityChanged = _previousEntityId != EntityId || _previousEntityType != EntityType;
 239
 0240        if (entityChanged)
 241        {
 0242            _previousEntityId = EntityId;
 0243            _previousEntityType = EntityType;
 244
 245            // Clear state when entity changes
 0246            _estimate = null;
 0247            _errorMessage = "";
 248
 0249            await LoadSummaryData();
 250        }
 0251    }
 252
 253    private async Task LoadSummaryData()
 254    {
 0255        _errorMessage = "";
 256
 0257        if (EntityType == "Article")
 258        {
 0259            _articleSummaryData = await SummaryApi.GetSummaryAsync(EntityId);
 260
 0261            if (_articleSummaryData?.HasSummary == true)
 262            {
 0263                _selectedTemplateId = _articleSummaryData.TemplateId;
 0264                _customPrompt = _articleSummaryData.CustomPrompt;
 265            }
 266        }
 267        else
 268        {
 0269            _entitySummaryData = await SummaryApi.GetEntitySummaryAsync(EntityType, EntityId);
 270
 0271            if (_entitySummaryData?.HasSummary == true)
 272            {
 0273                _selectedTemplateId = _entitySummaryData.TemplateId;
 0274                _customPrompt = _entitySummaryData.CustomPrompt;
 275            }
 276        }
 0277    }
 278
 279    private async Task LoadEstimateData()
 280    {
 0281        if (_summaryData?.HasSummary == true)
 282        {
 283            // Already has summary, no need for estimate
 0284            return;
 285        }
 286
 0287        _isLoading = true;
 0288        _errorMessage = "";
 0289        StateHasChanged();
 290
 291        try
 292        {
 0293            if (EntityType == "Article")
 294            {
 0295                _estimate = await SummaryApi.GetEstimateAsync(EntityId);
 296            }
 297            else
 298            {
 0299                _estimate = await SummaryApi.GetEntityEstimateAsync(EntityType, EntityId);
 300            }
 301
 0302            LoadSettingsFromEstimate();
 0303        }
 0304        catch (Exception ex)
 305        {
 0306            _errorMessage = $"Failed to load estimate: {ex.Message}";
 0307        }
 308        finally
 309        {
 0310            _isLoading = false;
 0311            StateHasChanged();
 312        }
 0313    }
 314
 315    private void LoadSettingsFromEstimate()
 316    {
 0317        if (_estimate != null)
 318        {
 0319            _selectedTemplateId = _estimate.TemplateId
 0320                ?? _templates.FirstOrDefault(t => t.Name == "Default")?.Id
 0321                ?? _templates.FirstOrDefault()?.Id;
 0322            _customPrompt = _estimate.CustomPrompt;
 0323            _advancedExpanded = !string.IsNullOrEmpty(_customPrompt);
 324        }
 0325    }
 326
 327    private async Task GenerateSummary()
 328    {
 0329        _isLoading = true;
 0330        _loadingMessage = EntityType == "Article"
 0331            ? "Reading backlinks and crafting summary..."
 0332            : "Analyzing session notes...";
 0333        _errorMessage = "";
 0334        StateHasChanged();
 335
 336        try
 337        {
 0338            var request = new GenerateSummaryRequestDto
 0339            {
 0340                TemplateId = _selectedTemplateId,
 0341                CustomPrompt = string.IsNullOrWhiteSpace(_customPrompt) ? null : _customPrompt,
 0342                SaveConfiguration = _saveConfiguration
 0343            };
 344
 345            SummaryGenerationDto? result;
 346
 0347            if (EntityType == "Article")
 348            {
 0349                result = await SummaryApi.GenerateSummaryAsync(EntityId, request);
 0350                if (result?.Success == true)
 351                {
 0352                    _articleSummaryData = new ArticleSummaryDto
 0353                    {
 0354                        ArticleId = EntityId,
 0355                        Summary = result.Summary,
 0356                        GeneratedAt = result.GeneratedDate,
 0357                        TemplateId = _selectedTemplateId,
 0358                        TemplateName = _templates.FirstOrDefault(t => t.Id == _selectedTemplateId)?.Name
 0359                    };
 360                }
 361            }
 362            else
 363            {
 0364                result = await SummaryApi.GenerateEntitySummaryAsync(EntityType, EntityId, request);
 0365                if (result?.Success == true)
 366                {
 0367                    _entitySummaryData = new EntitySummaryDto
 0368                    {
 0369                        EntityId = EntityId,
 0370                        EntityType = EntityType,
 0371                        Summary = result.Summary,
 0372                        GeneratedAt = result.GeneratedDate,
 0373                        TemplateId = _selectedTemplateId,
 0374                        TemplateName = _templates.FirstOrDefault(t => t.Id == _selectedTemplateId)?.Name
 0375                    };
 376                }
 377            }
 378
 0379            if (result?.Success == true)
 380            {
 0381                Snackbar.Add("Summary generated!", Severity.Success);
 382            }
 383            else
 384            {
 0385                _errorMessage = result?.ErrorMessage ?? "Failed to generate summary";
 0386                Snackbar.Add(_errorMessage, Severity.Error);
 387            }
 0388        }
 0389        catch (Exception ex)
 390        {
 0391            _errorMessage = ex.Message;
 0392            Snackbar.Add($"Error: {ex.Message}", Severity.Error);
 0393        }
 394        finally
 395        {
 0396            _isLoading = false;
 0397            StateHasChanged();
 398        }
 0399    }
 400
 401    private async Task RegenerateSummary()
 402    {
 0403        await ClearSummary();
 0404        await GenerateSummary();
 0405    }
 406
 407    private async Task ClearSummary()
 408    {
 409        bool success;
 410
 0411        if (EntityType == "Article")
 412        {
 0413            success = await SummaryApi.ClearSummaryAsync(EntityId);
 0414            _articleSummaryData = null;
 415        }
 416        else
 417        {
 0418            success = await SummaryApi.ClearEntitySummaryAsync(EntityType, EntityId);
 0419            _entitySummaryData = null;
 420        }
 421
 0422        if (success)
 423        {
 0424            await LoadSummaryData();
 0425            Snackbar.Add("Summary cleared", Severity.Info);
 426        }
 427        else
 428        {
 0429            Snackbar.Add("Failed to clear summary", Severity.Error);
 430        }
 0431    }
 432
 433    private async Task CopySummary()
 434    {
 0435        var summary = _summaryData?.Summary;
 0436        if (!string.IsNullOrEmpty(summary))
 437        {
 0438            await JSRuntime.InvokeVoidAsync("navigator.clipboard.writeText", summary);
 0439            Snackbar.Add("Copied to clipboard", Severity.Success);
 440        }
 0441    }
 442
 443    private async Task ToggleExpanded()
 444    {
 0445        IsExpanded = !IsExpanded;
 0446        await IsExpandedChanged.InvokeAsync(IsExpanded);
 447
 0448        if (IsExpanded)
 449        {
 450            // Panel is opening - load estimate if needed
 0451            await LoadEstimateData();
 452        }
 453        else
 454        {
 455            // Panel is closing - clear estimate to force fresh fetch on next open
 0456            _estimate = null;
 0457            _errorMessage = "";
 458        }
 0459    }
 460
 461    private string GetRelativeTime(DateTime date)
 462    {
 0463        var span = DateTime.UtcNow - date;
 464
 0465        if (span.TotalMinutes < 1) return "just now";
 0466        if (span.TotalMinutes < 60) return $"{(int)span.TotalMinutes}m ago";
 0467        if (span.TotalHours < 24) return $"{(int)span.TotalHours}h ago";
 0468        if (span.TotalDays < 7) return $"{(int)span.TotalDays}d ago";
 469
 0470        return date.ToLocalTime().ToString("MMM d");
 471    }
 472
 473    private string GetEstimateTooltip()
 474    {
 0475        if (_estimate == null) return "";
 0476        return $"~{_estimate.EstimatedInputTokens:N0} input tokens, ~{_estimate.EstimatedOutputTokens:N0} output tokens"
 477    }
 478
 479    /// <summary>
 480    /// Helper class to unify access to Article and Entity summary data
 481    /// </summary>
 482    private class SummaryDataAccessor
 483    {
 0484        public string? Summary { get; }
 0485        public DateTime? GeneratedAt { get; }
 0486        public bool HasSummary { get; }
 0487        public string? TemplateName { get; }
 0488        public Guid? TemplateId { get; }
 0489        public string? CustomPrompt { get; }
 490
 0491        public SummaryDataAccessor(ArticleSummaryDto dto)
 492        {
 0493            Summary = dto.Summary;
 0494            GeneratedAt = dto.GeneratedAt;
 0495            HasSummary = dto.HasSummary;
 0496            TemplateName = dto.TemplateName;
 0497            TemplateId = dto.TemplateId;
 0498            CustomPrompt = dto.CustomPrompt;
 0499        }
 500
 0501        public SummaryDataAccessor(EntitySummaryDto dto)
 502        {
 0503            Summary = dto.Summary;
 0504            GeneratedAt = dto.GeneratedAt;
 0505            HasSummary = dto.HasSummary;
 0506            TemplateName = dto.TemplateName;
 0507            TemplateId = dto.TemplateId;
 0508            CustomPrompt = dto.CustomPrompt;
 0509        }
 510    }
 511}