feat(ui): implement client-side [PAYWALL_TRIGGER] token parser, styling and tests
This commit is contained in:
@@ -8,7 +8,7 @@
|
||||
@inject NavigationManager NavigationManager
|
||||
|
||||
<div class="message-row @(Message.Sender == "User" ? "user-row" : "ai-row")">
|
||||
<div class="message-avatar">
|
||||
<div class="message-avatar" aria-hidden="true">
|
||||
@if (Message.Sender == "User")
|
||||
{
|
||||
<i class="bi bi-person-fill"></i>
|
||||
@@ -32,10 +32,10 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
@if (Message.IsPaywalled && !_isUnlocked)
|
||||
@if (_hasPaywall)
|
||||
{
|
||||
<div class="clear-teaser">
|
||||
@foreach (var segment in ParseSegments(Message.ClearText))
|
||||
<div class="paywall-teaser" aria-hidden="true">
|
||||
@foreach (var segment in ParseSegments(_displayTeaserText))
|
||||
{
|
||||
@if (segment.IsCitation)
|
||||
{
|
||||
@@ -48,47 +48,31 @@
|
||||
}
|
||||
</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-card" role="alert" aria-live="polite">
|
||||
<div class="upsell-header">
|
||||
<span class="upsell-icon">🔒</span>
|
||||
<h4>Zablokowano pełną odpowiedź (Paywall)</h4>
|
||||
<span class="upsell-icon" aria-hidden="true">🔒</span>
|
||||
<h4>Dostęp Premium Zablokowany</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.
|
||||
Twoje zasoby odpowiadają na to pytanie w <strong>@_localScore%</strong>. W materiale <strong>'@_lockedBookTitle'</strong> znaleźliśmy odpowiedź dopasowaną w <strong>@_globalScore%</strong>.
|
||||
</p>
|
||||
|
||||
<div class="upsell-actions">
|
||||
@if (_isSimulatingPayment)
|
||||
{
|
||||
<button class="btn-upsell btn-primary loading" disabled>
|
||||
<div class="payment-spinner"></div>
|
||||
<button class="btn-upsell btn-primary loading" disabled aria-busy="true">
|
||||
<div class="payment-spinner" aria-hidden="true"></div>
|
||||
PRZETWARZANIE PŁATNOŚCI...
|
||||
</button>
|
||||
}
|
||||
else
|
||||
{
|
||||
<button class="btn-upsell btn-primary" @onclick="HandlePurchase">
|
||||
KUP PUBLIKACJĘ (29 PLN)
|
||||
ODBLOKUJ PEŁNĄ TREŚĆ (29 PLN)
|
||||
</button>
|
||||
}
|
||||
<a href="/catalog" target="_blank" class="btn-upsell btn-secondary">
|
||||
<a href="/catalog?bookId=@_lockedBookId" class="btn-upsell btn-secondary">
|
||||
Zobacz szczegóły w Katalogu
|
||||
</a>
|
||||
</div>
|
||||
@@ -97,7 +81,7 @@
|
||||
else
|
||||
{
|
||||
<div class="full-response">
|
||||
@foreach (var segment in Message.Segments)
|
||||
@foreach (var segment in ParseSegments(GetCleanText()))
|
||||
{
|
||||
@if (segment.IsCitation)
|
||||
{
|
||||
@@ -112,8 +96,8 @@
|
||||
|
||||
@if (_showSuccessBanner)
|
||||
{
|
||||
<div class="success-unlock-banner">
|
||||
<span class="success-icon">✓</span>
|
||||
<div class="success-unlock-banner" role="status">
|
||||
<span class="success-icon" aria-hidden="true">✓</span>
|
||||
<span>Odblokowano pełną odpowiedź! Książka została dodana do Twojej biblioteki.</span>
|
||||
</div>
|
||||
}
|
||||
@@ -126,17 +110,61 @@
|
||||
@code {
|
||||
[Parameter] public ChatMessage Message { get; set; } = default!;
|
||||
[Parameter] public List<LastReadBookDto>? OwnedBooks { get; set; }
|
||||
[Parameter] public EventCallback<Guid> OnUnlockRequested { get; set; }
|
||||
|
||||
private string GetBubbleClass()
|
||||
{
|
||||
if (Message.Sender == "User") return "user-bubble";
|
||||
return Message.IsPaywalled && !_isUnlocked ? "ai-bubble paywalled-bubble" : "ai-bubble";
|
||||
}
|
||||
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;
|
||||
@@ -149,21 +177,26 @@
|
||||
|
||||
try
|
||||
{
|
||||
var bookTitle = string.IsNullOrEmpty(Message.SourceBookTitle)
|
||||
var bookTitle = string.IsNullOrEmpty(_lockedBookTitle)
|
||||
? "Architektura .NET 10 i Ekosystem Blazor"
|
||||
: Message.SourceBookTitle;
|
||||
: _lockedBookTitle;
|
||||
|
||||
// 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;
|
||||
_hasPaywall = false;
|
||||
_showSuccessBanner = true;
|
||||
|
||||
// Fetch updated library list and update state manager
|
||||
var updatedBooks = await Http.GetFromJsonAsync<List<LastReadBookDto>>("api/library/books");
|
||||
LibraryStateService.OwnedBooks = updatedBooks;
|
||||
|
||||
if (OnUnlockRequested.HasDelegate)
|
||||
{
|
||||
await OnUnlockRequested.InvokeAsync(_lockedBookId);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user