feat(search/rag): implement NexusSearchBox, dynamic Qdrant collection auto-provisioning, batch vector ingestion, mobile Serilog logging, and resolve 401 auth handler error (#51)

Resolves #52

This Pull Request introduces the **NexusSearchBox** search feature with premium unified styling, implements a robust **dynamic Qdrant collection auto-provisioning and batch-vector ingestion pipeline**, integrates a unified **Serilog logging infrastructure** for the Blazor Hybrid environment (MAUI), and resolves the **401 Unauthorized API header propagation error** inside mobile builds.

### 🚀 Key Implementations

#### 1. Premium `NexusSearchBox` & Semantic Search UI
* **NexusSearchBox Component:** Created an elegant search-as-you-type search box with smooth key navigation, quick-clearing, and seamless dynamic styling.
* **Unified Aesthetics:** Refactored the search box isolated styling to align perfectly with the dashboard's design system using glassmorphism, `--nexus-neon` token gradients, and smooth pulse/fade animations.
* **Semantic Search Integration:** Integrated semantic search query dispatching (`SearchLibrarySemanticallyQuery`) and wired up navigation seamlessly through the updated `ReaderNavigationService`.
* **Tests Hardening:** Added/adapted query assertions in `QueryTests.cs` to guarantee safe parameterization and error boundary mapping.

#### 2. Qdrant Collection Provisioning & Vector Ingestion
* **Dynamic Auto-Provisioning:** Implemented dynamic checking and lazy-creation of the `knowledge_units` collection using 768 dimensions and Cosine distance.
* **High-Performance Ingestion:** Optimized `ProcessKnowledgeUnitsAsync` with high-performance batch embedding generation using `_embeddingGenerator` and deterministic MD5 GUIDs for stable, duplicate-free upsertion.
* **Database Cache Clear Sync:** Integrated Qdrant collection deletion in `ClearCacheAsync` to ensure absolute consistency between the PostgreSQL database cache and vector database indices.

#### 3. Cross-Platform MAUI Logging (Serilog Infrastructure)
* **Serilog Integration:** Configured cross-platform Serilog routing in `SerilogConfiguration.cs`, streaming diagnostic logs safely across native platforms and the Blazor Webview container.
* **Interop Bridge:** Built `BlazorLoggingBridge.cs` to capture web console messages and pipe them directly to the native host logger.
* **Demo Interface:** Added an interactive `SerilogDemo.razor` sandbox under Pages.

#### 4. Resolving 401 Load Errors (Authentication Handler Flow)
* **Authentication Header Handler:** Implemented the `MobileAuthenticationHeaderHandler` to correctly extract, validate, and inject bearer JWT tokens into outbound API requests.
* **Configuration-based API Host:** Structured standard API URI routing to use clean configuration bindings in `appsettings.json`.

---

### 🧪 Verification & Build Status
* Run `dotnet build` from the solution root: Successfully compiled the full multi-targeted solution (`Liczba błędów: 0`).
* All unit and integration tests successfully executed and verified (`dotnet test`).

---------

Co-authored-by: Marek Jasiński <jasins.marek@gmail.com>
Co-authored-by: Marek Jaisński <jasins.marek@gmail.com>
Reviewed-on: #51
Co-authored-by: Antigravity <antigravity@google.com>
Co-committed-by: Antigravity <antigravity@google.com>
This commit was merged in pull request #51.
This commit is contained in:
2026-05-26 12:15:28 +00:00
committed by Marek Jaisński
parent 758b152a0c
commit a0bf6c15f4
61 changed files with 5111 additions and 570 deletions
+131 -22
View File
@@ -5,7 +5,10 @@
@using NexusReader.UI.Shared.Services
@inject IIdentityService IdentityService
@inject NavigationManager NavigationManager
@inject ISyncService SyncService
@attribute [Authorize]
@implements IDisposable
<PageTitle>Dashboard | Nexus Reader</PageTitle>
@@ -18,20 +21,20 @@
<img src="https://api.dicebear.com/7.x/bottts/svg?seed=Nexus" alt="Profile" class="profile-img" />
<div class="avatar-glow"></div>
</div>
<h1 class="username">[User_Explorer1988]</h1>
<h1 class="username">@(string.IsNullOrEmpty(_profile?.DisplayName) ? (_profile?.Email.Split('@')[0] ?? "Użytkownik") : _profile.DisplayName)</h1>
<div class="status-pills">
<div class="status-pill">
<span class="pill-label">Books Read:</span>
<span class="pill-value">12</span>
<span class="pill-label">Książki:</span>
<span class="pill-value">@(_profile?.BooksReadCount ?? 0)</span>
</div>
<div class="status-pill">
<span class="pill-label">Concepts Mapped:</span>
<span class="pill-value">450</span>
<span class="pill-label">Pojęcia:</span>
<span class="pill-value">@(_profile?.ConceptsMappedCount ?? 0)</span>
</div>
<div class="status-pill">
<span class="pill-label">Quiz Mastery:</span>
<span class="pill-value">88%</span>
<span class="pill-label">Średni Wynik:</span>
<span class="pill-value">@(_profile?.AverageQuizScore ?? 0)%</span>
</div>
</div>
</div>
@@ -39,7 +42,7 @@
<!-- Main Content Area -->
<main class="dashboard-content">
<h2 class="section-title">Witaj, @(_profile?.Email.Split('@')[0] ?? "Użytkowniku")</h2>
<h2 class="section-title">Witaj, @(string.IsNullOrEmpty(_profile?.DisplayName) ? (_profile?.Email.Split('@')[0] ?? "Użytkowniku") : _profile.DisplayName)</h2>
<div class="main-grid">
<!-- Current Reading Card -->
@@ -49,34 +52,88 @@
<!-- Knowledge Integration -->
<section class="integration-card glass-panel">
<div class="panel-header">
<h4>Knowledge Integration Progress</h4>
<h4>Integracja Wiedzy</h4>
<NexusIcon Name="arrow-right" Size="16" />
</div>
<div class="graph-placeholder">
<div class="graph-node central"></div>
<div class="graph-node satellite" style="--angle: 0deg; --dist: 60px;"></div>
<div class="graph-node satellite" style="--angle: 120deg; --dist: 50px;"></div>
<div class="graph-node satellite" style="--angle: 240deg; --dist: 70px;"></div>
<div class="active-node-label">TU JESTEŚ</div>
<div class="graph-node central" title="Ośrodek Wiedzy Nexus Reader"></div>
@if (_profile?.MappedConcepts != null && _profile.MappedConcepts.Any())
{
@for (int i = 0; i < _profile.MappedConcepts.Count; i++)
{
var concept = _profile.MappedConcepts[i];
var angle = i * (360.0 / _profile.MappedConcepts.Count);
var dist = 65;
<div class="graph-node satellite"
style="--angle: @(angle)deg; --dist: @(dist)px;"
title="[@concept.Type] @concept.Content"
@onmouseover="() => SetHoveredConcept(concept)"
@onmouseout="ClearHoveredConcept">
</div>
}
}
else
{
<div class="graph-node satellite" style="--angle: 0deg; --dist: 60px;"></div>
<div class="graph-node satellite" style="--angle: 120deg; --dist: 50px;"></div>
<div class="graph-node satellite" style="--angle: 240deg; --dist: 70px;"></div>
}
<div class="active-node-label">
@(string.IsNullOrEmpty(_hoveredConceptLabel) ? "TU JESTEŚ" : _hoveredConceptLabel)
</div>
</div>
@if (_hoveredConcept != null)
{
<div class="concept-detail-toast">
<span class="concept-type">@_hoveredConcept.Type</span>
<p class="concept-content">@_hoveredConcept.Content</p>
</div>
}
else
{
<div class="concept-detail-toast placeholder">
<span class="concept-type">Mapowanie AI</span>
<p class="concept-content">Najedź na węzeł, aby zbadać pojęcie wydobyte przez Nexus AI.</p>
</div>
}
</section>
<!-- Quiz Summary -->
<section class="quiz-card glass-panel">
<div class="panel-header">
<h4>Quiz Summary: Key Thinkers</h4>
<h4>Rozwiązane Quizy</h4>
<NexusIcon Name="arrow-right" Size="16" />
</div>
<div class="quiz-preview">
<p class="question">Który artysta namalował 'Ostatnią Wieczerzę'?</p>
<div class="quiz-options">
<div class="quiz-option active">
<span class="option-letter">A)</span> Michal Anioł
@if (_profile?.RecentQuizzes != null && _profile.RecentQuizzes.Any())
{
<div class="quiz-history-list">
@foreach (var quiz in _profile.RecentQuizzes)
{
<div class="quiz-history-item">
<div class="quiz-item-header">
<span class="quiz-topic">@quiz.Topic</span>
<span class="quiz-score badge @(quiz.Percentage >= 80 ? "badge-success" : quiz.Percentage >= 50 ? "badge-warning" : "badge-danger")">
@quiz.Score / @quiz.TotalQuestions (@((int)quiz.Percentage)%)
</span>
</div>
<div class="quiz-item-meta">
<span class="quiz-date">@quiz.CompletedDate.ToString("g")</span>
</div>
</div>
}
</div>
<div class="quiz-option">
<span class="option-letter">B)</span> Leonardo da Vinci
}
else
{
<div class="empty-quiz-state">
<p class="question">Brak rozwiązanych quizów</p>
<p class="sub-text">Rozwiązuj quizy w trakcie czytania książek, aby śledzić swoje postępy.</p>
</div>
</div>
}
</div>
</section>
</div>
@@ -86,13 +143,65 @@
@code {
private UserProfileDto? _profile;
private MappedConceptDto? _hoveredConcept;
private string _hoveredConceptLabel = string.Empty;
protected override async Task OnInitializedAsync()
{
IdentityService.OnStateInvalidated += HandleStateInvalidatedAsync;
await LoadProfileAsync();
await SyncService.InitializeAsync();
SyncService.OnProgressReceived += HandleProgressReceivedAsync;
}
private void SetHoveredConcept(MappedConceptDto concept)
{
_hoveredConcept = concept;
_hoveredConceptLabel = concept.DisplayLabel;
}
private void ClearHoveredConcept()
{
_hoveredConcept = null;
_hoveredConceptLabel = string.Empty;
}
private async Task LoadProfileAsync()
{
var result = await IdentityService.GetProfileAsync();
if (result.IsSuccess)
{
_profile = result.Value;
}
else
{
_profile = null;
}
StateHasChanged();
}
private async Task HandleStateInvalidatedAsync()
{
await InvokeAsync(async () =>
{
await LoadProfileAsync();
});
}
private async Task HandleProgressReceivedAsync(string pageId, DateTime timestamp)
{
await InvokeAsync(async () =>
{
IdentityService.ClearCache();
await LoadProfileAsync();
});
}
public void Dispose()
{
IdentityService.OnStateInvalidated -= HandleStateInvalidatedAsync;
SyncService.OnProgressReceived -= HandleProgressReceivedAsync;
}
}