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:
@@ -6,6 +6,7 @@ public interface IReaderNavigationService
|
||||
int CurrentChapterIndex { get; }
|
||||
int TotalChapters { get; }
|
||||
string ChapterTitle { get; }
|
||||
string? PendingScrollBlockId { get; set; }
|
||||
|
||||
event Func<Task>? OnNavigationChanged;
|
||||
|
||||
|
||||
@@ -7,5 +7,6 @@ public interface ISyncService
|
||||
Task<Result> InitializeAsync();
|
||||
Task<Result> UpdateProgressAsync(string pageId, Guid ebookId, double progress, string? chapterTitle, int chapterIndex);
|
||||
event Func<string, DateTime, Task> OnProgressReceived;
|
||||
event Func<string, double, Task>? OnIngestionProgressReceived;
|
||||
Task DisposeAsync();
|
||||
}
|
||||
|
||||
@@ -249,6 +249,25 @@ public class IdentityService : IIdentityService
|
||||
}
|
||||
}
|
||||
|
||||
public void ClearCache()
|
||||
{
|
||||
_cachedProfile = null;
|
||||
if (OnStateInvalidated != null)
|
||||
{
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await OnStateInvalidated.Invoke();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore exceptions from event handlers
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private class LoginResponse
|
||||
{
|
||||
public string TokenType { get; set; } = string.Empty;
|
||||
|
||||
@@ -17,6 +17,8 @@ public sealed partial class KnowledgeCoordinator : IDisposable
|
||||
private readonly IReaderInteractionService _interactionService;
|
||||
private readonly ILogger<KnowledgeCoordinator> _logger;
|
||||
|
||||
public string CurrentFullPageContent { get; private set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Raised when the knowledge graph has been updated with new data.
|
||||
/// Subscribers must return a Task to enable proper async handling.
|
||||
@@ -77,6 +79,7 @@ public sealed partial class KnowledgeCoordinator : IDisposable
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(fullContent)) return;
|
||||
|
||||
CurrentFullPageContent = fullContent;
|
||||
LogGeneratingGraph(tenantId);
|
||||
|
||||
await _graphService.Clear();
|
||||
@@ -94,11 +97,15 @@ public sealed partial class KnowledgeCoordinator : IDisposable
|
||||
if (OnGraphUpdated != null)
|
||||
await OnGraphUpdated.Invoke(packet.Graph);
|
||||
await _platformService.VibrateSuccessAsync();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await _graphService.SetLoading(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await _graphService.SetLoading(false);
|
||||
LogGraphError(ex, tenantId);
|
||||
}
|
||||
}
|
||||
@@ -144,6 +151,7 @@ public sealed partial class KnowledgeCoordinator : IDisposable
|
||||
|
||||
public async Task ClearAsync()
|
||||
{
|
||||
CurrentFullPageContent = string.Empty;
|
||||
await _graphService.Clear();
|
||||
await _quizService.SetQuiz(null, null);
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ public class ReaderNavigationService : IReaderNavigationService
|
||||
public int CurrentChapterIndex { get; private set; } = 0;
|
||||
public int TotalChapters { get; private set; } = 1;
|
||||
public string ChapterTitle { get; private set; } = "Loading...";
|
||||
public string? PendingScrollBlockId { get; set; }
|
||||
|
||||
public event Func<Task>? OnNavigationChanged;
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ public class SyncService : ISyncService, IAsyncDisposable
|
||||
private CancellationTokenSource? _debounceCts;
|
||||
|
||||
public event Func<string, DateTime, Task>? OnProgressReceived;
|
||||
public event Func<string, double, Task>? OnIngestionProgressReceived;
|
||||
|
||||
public SyncService(
|
||||
HttpClient httpClient,
|
||||
@@ -50,9 +51,20 @@ public class SyncService : ISyncService, IAsyncDisposable
|
||||
_hubConnection.On<string, DateTime>("ProgressUpdated", async (pageId, timestamp) =>
|
||||
{
|
||||
// Note: In the future we might want to receive ebookId and progress here too
|
||||
if (pageId == _lastSentPageId)
|
||||
{
|
||||
_logger.LogDebug("[Sync] Ignoring self progress update for page {PageId}.", pageId);
|
||||
return;
|
||||
}
|
||||
_lastSentPageId = pageId; // Prevent echoing back duplicate progress updates
|
||||
if (OnProgressReceived != null) await OnProgressReceived(pageId, timestamp);
|
||||
});
|
||||
|
||||
_hubConnection.On<string, double>("IngestionProgress", async (message, progress) =>
|
||||
{
|
||||
if (OnIngestionProgressReceived != null) await OnIngestionProgressReceived(message, progress);
|
||||
});
|
||||
|
||||
try
|
||||
{
|
||||
await _hubConnection.StartAsync();
|
||||
@@ -71,6 +83,8 @@ public class SyncService : ISyncService, IAsyncDisposable
|
||||
{
|
||||
if (pageId == _lastSentPageId) return Result.Ok();
|
||||
|
||||
_lastSentPageId = pageId;
|
||||
|
||||
// Proper trailing-edge debounce
|
||||
_debounceCts?.Cancel();
|
||||
_debounceCts = new CancellationTokenSource();
|
||||
@@ -86,8 +100,7 @@ public class SyncService : ISyncService, IAsyncDisposable
|
||||
|
||||
if (_hubConnection?.State == HubConnectionState.Connected)
|
||||
{
|
||||
await _hubConnection.SendAsync("UpdateProgress", pageId, ebookId, progress, chapterTitle, token);
|
||||
_lastSentPageId = pageId;
|
||||
await _hubConnection.SendAsync("UpdateProgress", pageId, ebookId, progress, chapterTitle, chapterIndex);
|
||||
}
|
||||
}
|
||||
catch (TaskCanceledException) { /* Ignored, user kept scrolling */ }
|
||||
|
||||
Reference in New Issue
Block a user