feat(intelligence): implement Global AI Q&A screen and paywall blocker

- Implemented standard empty and active chat conversation states for the `/intelligence` page
- Created interactive `AiResponseRenderer` with AOT-compliant sentence splitting and payment gateway simulation
- Added scoped `LibraryStateService` to synchronize book ownership and updates across the application
- Obfuscated paywalled content in DOM to prevent inspection bypass
- Fixed local port connection mismatch by updating API configurations to use port 5104
This commit is contained in:
2026-06-06 10:41:48 +02:00
parent bcd5daa3a0
commit faf6ec826e
15 changed files with 932 additions and 318 deletions
@@ -0,0 +1,244 @@
@using NexusReader.UI.Shared.Models
@using NexusReader.UI.Shared.Services
@using NexusReader.Application.DTOs.AI
@using NexusReader.Application.DTOs.User
@using System.Net.Http.Json
@inject HttpClient Http
@inject ILibraryStateService LibraryStateService
@inject NavigationManager NavigationManager
<div class="message-row @(Message.Sender == "User" ? "user-row" : "ai-row")">
<div class="message-avatar">
@if (Message.Sender == "User")
{
<i class="bi bi-person-fill"></i>
}
else
{
<i class="bi bi-robot"></i>
}
</div>
<div class="message-bubble @GetBubbleClass()">
<div class="message-header">
<span class="sender-name">@Message.Sender</span>
<span class="message-time">@Message.Timestamp.ToString("HH:mm")</span>
</div>
<div class="message-content">
@if (Message.Sender == "User")
{
<p>@Message.Text</p>
}
else
{
@if (Message.IsPaywalled && !_isUnlocked)
{
<div class="clear-teaser">
@foreach (var segment in ParseSegments(Message.ClearText))
{
@if (segment.IsCitation)
{
<NexusCitationMarker SourceId="@segment.CitationId" Citations="@Message.Citations" />
}
else
{
@RenderMarkdown(segment.Text)
}
}
</div>
<div class="teaser-blur">
@foreach (var segment in ParseSegments(Message.BlurredTeaserText))
{
@if (segment.IsCitation)
{
<NexusCitationMarker SourceId="@segment.CitationId" Citations="@Message.Citations" />
}
else
{
@RenderMarkdown(segment.Text)
}
}
</div>
<div class="upsell-card">
<div class="upsell-header">
<span class="upsell-icon">🔒</span>
<h4>Zablokowano pełną odpowiedź (Paywall)</h4>
</div>
<p class="upsell-text">
Powyższy fragment wiedzy pochodzi z materiału:
<strong>'@(string.IsNullOrEmpty(Message.SourceBookTitle) ? "Architektura .NET 10 i Ekosystem Blazor" : Message.SourceBookTitle)'</strong>.
Ta pozycja nie znajduje się w Twojej bibliotece (/my-books). Aby uzyskać dostęp do pełnych instrukcji technicznych oraz gotowych kodów C#, odblokuj ten materiał w katalogu.
</p>
<div class="upsell-actions">
@if (_isSimulatingPayment)
{
<button class="btn-upsell btn-primary loading" disabled>
<div class="payment-spinner"></div>
PRZETWARZANIE PŁATNOŚCI...
</button>
}
else
{
<button class="btn-upsell btn-primary" @onclick="HandlePurchase">
KUP PUBLIKACJĘ (29 PLN)
</button>
}
<a href="/catalog" target="_blank" class="btn-upsell btn-secondary">
Zobacz szczegóły w Katalogu
</a>
</div>
</div>
}
else
{
<div class="full-response">
@foreach (var segment in Message.Segments)
{
@if (segment.IsCitation)
{
<NexusCitationMarker SourceId="@segment.CitationId" Citations="@Message.Citations" />
}
else
{
@RenderMarkdown(segment.Text)
}
}
</div>
@if (_showSuccessBanner)
{
<div class="success-unlock-banner">
<span class="success-icon">✓</span>
<span>Odblokowano pełną odpowiedź! Książka została dodana do Twojej biblioteki.</span>
</div>
}
}
}
</div>
</div>
</div>
@code {
[Parameter] public ChatMessage Message { get; set; } = default!;
[Parameter] public List<LastReadBookDto>? OwnedBooks { get; set; }
private string GetBubbleClass()
{
if (Message.Sender == "User") return "user-bubble";
return Message.IsPaywalled && !_isUnlocked ? "ai-bubble paywalled-bubble" : "ai-bubble";
}
private bool _isUnlocked = false;
private bool _isSimulatingPayment = false;
private bool _showSuccessBanner = false;
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(Message.SourceBookTitle)
? "Architektura .NET 10 i Ekosystem Blazor"
: Message.SourceBookTitle;
// Call POST endpoint to persist the purchase
var response = await Http.PostAsJsonAsync("api/library/purchase", new { Title = bookTitle });
if (response.IsSuccessStatusCode)
{
_isUnlocked = true;
Message.IsPaywalled = false;
_showSuccessBanner = true;
// Fetch updated library list and update state manager
var updatedBooks = await Http.GetFromJsonAsync<List<LastReadBookDto>>("api/library/books");
LibraryStateService.OwnedBooks = updatedBooks;
}
else
{
Console.WriteLine("[AiResponseRenderer] Purchase failed on server.");
}
}
catch (Exception ex)
{
Console.WriteLine($"[AiResponseRenderer] Error processing purchase: {ex.Message}");
}
finally
{
_isSimulatingPayment = false;
StateHasChanged();
}
}
private List<ResponseSegment> ParseSegments(string text)
{
var segments = new List<ResponseSegment>();
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, @"\*\*(.*?)\*\*", "<strong>$1</strong>");
html = System.Text.RegularExpressions.Regex.Replace(html, @"\*(.*?)\*", "<em>$1</em>");
html = System.Text.RegularExpressions.Regex.Replace(html, @"```(?:[a-zA-Z0-9+#]+)?\s*([\s\S]*?)\s*```", "<pre class=\"nexus-code-block\"><code>$1</code></pre>");
html = System.Text.RegularExpressions.Regex.Replace(html, @"`(.*?)`", "<code class=\"nexus-inline-code\">$1</code>");
html = html.Replace("\n", "<br />");
return new MarkupString(html);
}
}