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:
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user