ce4687ee93
- Add ILogger<GetContextualRecommendationsQueryHandler> with structured logging - Guard empty embedding text in VectorSearchStore (return empty vector, skip search) - Benchmark vector search and embedding latency with Stopwatch (LogDebug/LogInfo) - Refine EnsureCollectionExistsAsync: log creation events and non-fatal errors - Replace all Console.WriteLine with ILogger in UI components (AiAssistantBubble, AiResponseRenderer, IntelligenceToolbar, SelectionAiPanel, Catalog, Intelligence, MyBooks) - Create IRecommendationService abstraction + RecommendationService WASM implementation - Register IRecommendationService in Web.Client DI - Add ContextualRecommendationsWidget component with loading spinner and design tokens - Add ContextualRecommendationsWidget to Dashboard.razor - Update test constructor with ILogger mock for GetContextualRecommendationsQueryHandler Closes review items: 2, 3, 4, 5, 6, 7, 8, 9, 10 Item 1 (unit tests) was already completed in previous session
172 lines
6.1 KiB
Plaintext
172 lines
6.1 KiB
Plaintext
@page "/my-books"
|
|
@attribute [Authorize]
|
|
@implements IDisposable
|
|
@using NexusReader.UI.Shared.Components.Organisms
|
|
@using NexusReader.Application.DTOs.User
|
|
@using NexusReader.UI.Shared.Services
|
|
@using Microsoft.Extensions.Logging
|
|
@using System.Net.Http.Json
|
|
@inject HttpClient Http
|
|
@inject IReaderNavigationService ReaderNavigation
|
|
@inject ILibraryStateService LibraryStateService
|
|
@inject ILogger<MyBooks> Logger
|
|
|
|
<div class="my-books-page">
|
|
<header class="my-books-header">
|
|
<div class="header-title-section">
|
|
<h1>Moje Książki</h1>
|
|
<p class="subtitle">Twoje aktywne lektury i postępy w nauce z Nexus AI</p>
|
|
</div>
|
|
<AuthorizeView Roles="Admin, ContentManager">
|
|
<button class="btn-nexus add-book-trigger" @onclick="() => _isModalOpen = true">
|
|
<span class="btn-icon">+</span> Dodaj E-book
|
|
</button>
|
|
</AuthorizeView>
|
|
</header>
|
|
|
|
<BookIngestionModal @bind-IsOpen="_isModalOpen" @bind-IsOpen:after="RefreshLibrary" />
|
|
|
|
<div class="my-books-content">
|
|
@if (_isLoading)
|
|
{
|
|
<div class="my-books-loading-container">
|
|
<div class="loader-card">
|
|
<div class="spinner-glow small"></div>
|
|
<span class="loader-text">Wczytywanie biblioteki...</span>
|
|
</div>
|
|
|
|
<div class="loading-grid">
|
|
@for (int i = 0; i < 3; i++)
|
|
{
|
|
<div class="skeleton-card">
|
|
<div class="skeleton-cover"></div>
|
|
<div class="skeleton-details">
|
|
<div class="skeleton-line title"></div>
|
|
<div class="skeleton-line author"></div>
|
|
<div class="skeleton-line progress"></div>
|
|
</div>
|
|
</div>
|
|
}
|
|
</div>
|
|
</div>
|
|
}
|
|
else if (_books == null || !_books.Any())
|
|
{
|
|
<div class="empty-state-container">
|
|
<div class="empty-icon-pulse">
|
|
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"></path>
|
|
<path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"></path>
|
|
</svg>
|
|
</div>
|
|
<h3>Pusta biblioteka</h3>
|
|
<p>Nie masz jeszcze żadnych książek na swojej półce. Przejdź do katalogu, aby rozpocząć kurs.</p>
|
|
<a href="/catalog" class="btn-nexus catalog-btn">Przeglądaj Katalog</a>
|
|
</div>
|
|
}
|
|
else
|
|
{
|
|
<div class="books-grid">
|
|
@foreach (var book in _books)
|
|
{
|
|
<div class="book-card" @onclick="() => OpenBook(book.Id)">
|
|
<div class="book-cover-container">
|
|
<img src="@(book.CoverUrl ?? "https://api.dicebear.com/7.x/identicon/svg?seed=" + book.Title)" alt="@book.Title" class="book-cover" />
|
|
<div class="cover-overlay">
|
|
<span class="read-action">Czytaj teraz</span>
|
|
</div>
|
|
</div>
|
|
<div class="book-details">
|
|
<h3 class="book-title" title="@book.Title">@book.Title</h3>
|
|
<p class="book-author">@book.Author.Name</p>
|
|
|
|
@if (book.Progress > 0)
|
|
{
|
|
<div class="book-progress-section">
|
|
<div class="progress-bar">
|
|
<div class="progress-fill" style="width: @(book.Progress.ToString("F0"))%"></div>
|
|
</div>
|
|
<span class="progress-text">Postęp: @(book.Progress.ToString("F0"))% (@book.LastChapter)</span>
|
|
</div>
|
|
}
|
|
else
|
|
{
|
|
<span class="new-badge">Nowa</span>
|
|
}
|
|
|
|
<div class="card-actions">
|
|
<button class="btn-nexus primary-accent-btn">
|
|
KONTYNUUJ KURS
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
</div>
|
|
}
|
|
</div>
|
|
</div>
|
|
|
|
@code {
|
|
private bool _isModalOpen;
|
|
private bool _isLoading = true;
|
|
private List<LastReadBookDto>? _books;
|
|
|
|
protected override void OnInitialized()
|
|
{
|
|
LibraryStateService.OnBooksChanged += HandleBooksChanged;
|
|
}
|
|
|
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
|
{
|
|
if (firstRender)
|
|
{
|
|
await LoadBooksAsync();
|
|
}
|
|
}
|
|
|
|
private void HandleBooksChanged()
|
|
{
|
|
_ = InvokeAsync(LoadBooksAsync);
|
|
}
|
|
|
|
private async Task LoadBooksAsync()
|
|
{
|
|
_isLoading = true;
|
|
StateHasChanged();
|
|
|
|
try
|
|
{
|
|
_books = await Http.GetFromJsonAsync<List<LastReadBookDto>>("api/library/books");
|
|
_isLoading = false;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.LogError(ex, "[MyBooks] Failed to load books.");
|
|
if (OperatingSystem.IsBrowser())
|
|
{
|
|
_isLoading = false;
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
StateHasChanged();
|
|
}
|
|
}
|
|
|
|
private async Task RefreshLibrary()
|
|
{
|
|
await LoadBooksAsync();
|
|
}
|
|
|
|
private void OpenBook(Guid bookId)
|
|
{
|
|
ReaderNavigation.NavigateToBook(bookId);
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
LibraryStateService.OnBooksChanged -= HandleBooksChanged;
|
|
}
|
|
}
|