@using NexusReader.UI.Shared.Models @using NexusReader.UI.Shared.Services @using NexusReader.Application.DTOs.AI @using NexusReader.Application.DTOs.User @using Microsoft.Extensions.Logging @using System.Net.Http.Json @inject HttpClient Http @inject ILibraryStateService LibraryStateService @inject NavigationManager NavigationManager @inject ILogger Logger
@Message.Sender @Message.Timestamp.ToString("HH:mm")
@if (Message.Sender == "User") {

@Message.Text

} else { @if (_hasPaywall) { } else {
@foreach (var segment in ParseSegments(GetCleanText())) { @if (segment.IsCitation) { } else { @RenderMarkdown(segment.Text) } }
@if (_showSuccessBanner) {
Odblokowano pełną odpowiedź! Książka została dodana do Twojej biblioteki.
} } }
@code { [Parameter] public ChatMessage Message { get; set; } = default!; [Parameter] public List? OwnedBooks { get; set; } [Parameter] public EventCallback OnUnlockRequested { get; set; } private bool _hasPaywall; private string _displayTeaserText = string.Empty; private Guid _lockedBookId; private string _lockedBookTitle = string.Empty; private int _localScore; private int _globalScore; private bool _isUnlocked = false; private bool _isSimulatingPayment = false; private bool _showSuccessBanner = false; protected override void OnParametersSet() { base.OnParametersSet(); if (Message != null && Message.Sender != "User" && !_isUnlocked) { _hasPaywall = PaywallParser.TryParsePaywallTrigger(Message.Text, out _displayTeaserText, out _lockedBookId, out _lockedBookTitle, out _localScore, out _globalScore); // Additional check: if user already owns the book, don't show the paywall if (_hasPaywall && OwnedBooks != null) { var isOwned = OwnedBooks.Any(b => b.Id == _lockedBookId || (!string.IsNullOrEmpty(b.Title) && b.Title.Equals(_lockedBookTitle, StringComparison.OrdinalIgnoreCase))); if (isOwned) { _hasPaywall = false; } } } else { _hasPaywall = false; } } private string GetCleanText() { if (Message == null) return string.Empty; if (PaywallParser.TryParsePaywallTrigger(Message.Text, out var cleanText, out _, out _, out _, out _)) { return cleanText; } return Message.Text; } private string GetBubbleClass() { if (Message.Sender == "User") return "user-bubble"; return _hasPaywall ? "ai-bubble paywalled-bubble" : "ai-bubble"; } private async Task HandlePurchase() { if (_isSimulatingPayment) return; _isSimulatingPayment = true; StateHasChanged(); // Simulate payment gateway delay (1.5 seconds) await Task.Delay(1500); try { var bookTitle = string.IsNullOrEmpty(_lockedBookTitle) ? "Architektura .NET 10 i Ekosystem Blazor" : _lockedBookTitle; // Call POST endpoint to persist the purchase var response = await Http.PostAsJsonAsync("api/library/purchase", new { Title = bookTitle }); if (response.IsSuccessStatusCode) { _isUnlocked = true; _hasPaywall = false; _showSuccessBanner = true; // Fetch updated library list and update state manager var updatedBooks = await Http.GetFromJsonAsync>("api/library/books"); LibraryStateService.OwnedBooks = updatedBooks; if (OnUnlockRequested.HasDelegate) { await OnUnlockRequested.InvokeAsync(_lockedBookId); } } else { Logger.LogWarning("[AiResponseRenderer] Purchase failed on server for book {BookId}.", _lockedBookId); } } catch (Exception ex) { Logger.LogError(ex, "[AiResponseRenderer] Error processing purchase for book {BookId}.", _lockedBookId); } finally { _isSimulatingPayment = false; StateHasChanged(); } } private List ParseSegments(string text) { var segments = new List(); if (string.IsNullOrEmpty(text)) return segments; var regex = new System.Text.RegularExpressions.Regex( @"\[Source ID:\s*([^\]]+)\]|\[([a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12})\]", System.Text.RegularExpressions.RegexOptions.IgnoreCase); var matches = regex.Matches(text); int lastIndex = 0; foreach (System.Text.RegularExpressions.Match match in matches) { if (match.Index > lastIndex) { segments.Add(new ResponseSegment { Text = text.Substring(lastIndex, match.Index - lastIndex), IsCitation = false }); } var citationId = match.Groups[1].Success ? match.Groups[1].Value.Trim() : match.Groups[2].Value.Trim(); segments.Add(new ResponseSegment { IsCitation = true, CitationId = citationId }); lastIndex = match.Index + match.Length; } if (lastIndex < text.Length) { segments.Add(new ResponseSegment { Text = text.Substring(lastIndex), IsCitation = false }); } return segments; } private MarkupString RenderMarkdown(string text) { if (string.IsNullOrEmpty(text)) return new MarkupString(string.Empty); var html = System.Net.WebUtility.HtmlEncode(text); html = System.Text.RegularExpressions.Regex.Replace(html, @"\*\*(.*?)\*\*", "$1"); html = System.Text.RegularExpressions.Regex.Replace(html, @"\*(.*?)\*", "$1"); html = System.Text.RegularExpressions.Regex.Replace(html, @"```(?:[a-zA-Z0-9+#]+)?\s*([\s\S]*?)\s*```", "
$1
"); html = System.Text.RegularExpressions.Regex.Replace(html, @"`(.*?)`", "$1"); html = html.Replace("\n", "
"); return new MarkupString(html); } }