fe5ff81c98
## Overview This PR completes the architectural consolidation of the web project and stabilizes the Identity-based authentication flow for the NexusReader application. It also refines the UI aesthetic for the Book Ingestion Modal as requested in #33. ## Key Changes - **Project Consolidation**: Fully merged `NexusReader.Web.New` into `NexusReader.Web`. This includes updating all namespace references, VS Code launch/task configurations, and CI/CD (`Dockerfile`). - **Identity Stabilization**: - Implemented `IIdentityService` on the server using `SignInManager<NexusUser>` and `UserManager<NexusUser>`. - Fixed registration logic to include mandatory fields (`SubscriptionPlanId`, `TenantId`). - Updated `Login.razor` to force a page reload on successful login, ensuring proper synchronization of authentication cookies between SignalR and the browser. - **UI/UX Refinement**: - Updated `BookIngestionModal` styling to follow the **Nexus Neon** design system. - Added premium button styles with hover effects and glows. - Improved modal layout and interaction feedback (shimmer effects, spinner colors). - **Cleanup**: Removed obsolete interfaces and constants that were superseded by newer Application layer implementations. ## Verification - Successfully built the solution: `dotnet build NexusReader.slnx --no-restore` - Verified project structure and file moves. - Validated server-side authentication logic. Fixes #33 --------- Co-authored-by: Marek Jasiński <jasins.marek@gmail.com> Reviewed-on: #40 Co-authored-by: Antigravity <antigravity@google.com> Co-committed-by: Antigravity <antigravity@google.com>
148 lines
4.9 KiB
Plaintext
148 lines
4.9 KiB
Plaintext
@using NexusReader.UI.Shared.Services
|
|
@using NexusReader.Application.DTOs.AI
|
|
@inject IQuizStateService QuizState
|
|
@inject KnowledgeCoordinator Coordinator
|
|
@implements IDisposable
|
|
|
|
<div class="ai-bubble-container">
|
|
<div class="ai-bubble">
|
|
<div class="ai-avatar">
|
|
<div class="avatar-ring"></div>
|
|
<NexusIcon Name="robot" Size="48" Class="@(_isStreaming ? "neon-pulse" : "neon-glow")" />
|
|
<div class="avatar-label">
|
|
<span class="name">E-Czytnik</span>
|
|
<span class="role">Asystent AI</span>
|
|
</div>
|
|
</div>
|
|
<div class="ai-content">
|
|
@if (_isLoading)
|
|
{
|
|
<div class="loading-state">
|
|
<div class="shimmer">Analizuję fragment...</div>
|
|
</div>
|
|
}
|
|
else
|
|
{
|
|
<NexusTypography Variant="NexusTypography.TypographyVariant.UI">
|
|
@_displayedText@(_isStreaming ? "▍" : "")
|
|
</NexusTypography>
|
|
}
|
|
|
|
<div class="ai-actions">
|
|
<button class="action-btn ghost" @onclick='() => HandleActionClick("more")'>Pokaż więcej informacji</button>
|
|
<button class="action-btn neon-border" @onclick='() => HandleActionClick("quiz")' disabled="@(_isLoading)">Rozwiąż quiz</button>
|
|
</div>
|
|
</div>
|
|
<div class="bubble-pointer"></div>
|
|
</div>
|
|
</div>
|
|
|
|
@code {
|
|
[Parameter] public string ContextBlockId { get; set; } = string.Empty;
|
|
/// <summary>Fallback static dialogue shown when no live AI content is available.</summary>
|
|
[Parameter] public string Dialogue { get; set; } = string.Empty;
|
|
[Parameter] public List<string> Actions { get; set; } = new();
|
|
[Parameter] public string FullPageContent { get; set; } = string.Empty;
|
|
[Parameter] public EventCallback<string> OnActionTriggered { get; set; }
|
|
|
|
private string _displayedText = string.Empty;
|
|
private bool _isLoading = false;
|
|
private bool _isStreaming = false;
|
|
private string _lastFetchedBlockId = string.Empty;
|
|
private KnowledgePacket? _packet;
|
|
private CancellationTokenSource? _streamCts;
|
|
|
|
protected override async Task OnParametersSetAsync()
|
|
{
|
|
// Only re-fetch when the block context actually changes
|
|
if (string.IsNullOrEmpty(ContextBlockId) || ContextBlockId == _lastFetchedBlockId)
|
|
return;
|
|
|
|
_lastFetchedBlockId = ContextBlockId;
|
|
await FetchAndStreamAsync();
|
|
}
|
|
|
|
private async Task FetchAndStreamAsync()
|
|
{
|
|
// Cancel any in-progress stream
|
|
_streamCts?.Cancel();
|
|
_streamCts = new CancellationTokenSource();
|
|
var token = _streamCts.Token;
|
|
|
|
_isLoading = true;
|
|
_isStreaming = false;
|
|
_displayedText = string.Empty;
|
|
_packet = null;
|
|
StateHasChanged();
|
|
|
|
try
|
|
{
|
|
var contentToAnalyze = !string.IsNullOrWhiteSpace(FullPageContent)
|
|
? FullPageContent
|
|
: $"[ID: {ContextBlockId}]\n{Dialogue}";
|
|
|
|
var result = await Coordinator.RequestSummaryAndQuizAsync(contentToAnalyze);
|
|
_packet = result.IsSuccess ? result.Value : null;
|
|
|
|
var summary = _packet?.Summary;
|
|
|
|
if (string.IsNullOrWhiteSpace(summary))
|
|
{
|
|
// Fall back to the static Dialogue parameter
|
|
_displayedText = string.IsNullOrEmpty(Dialogue)
|
|
? "Brak danych do analizy."
|
|
: Dialogue;
|
|
_isLoading = false;
|
|
StateHasChanged();
|
|
return;
|
|
}
|
|
|
|
_isLoading = false;
|
|
_isStreaming = true;
|
|
|
|
// Word-by-word reveal (streaming simulation)
|
|
var words = summary.Split(' ');
|
|
foreach (var word in words)
|
|
{
|
|
if (token.IsCancellationRequested) break;
|
|
_displayedText += (string.IsNullOrEmpty(_displayedText) ? "" : " ") + word;
|
|
StateHasChanged();
|
|
await Task.Delay(40, token); // ~25 words/sec
|
|
}
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
// Superseded by a newer block — silently drop
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_displayedText = string.IsNullOrEmpty(Dialogue) ? "Błąd analizy." : Dialogue;
|
|
Console.WriteLine($"[AiAssistantBubble] Error fetching summary: {ex.Message}");
|
|
}
|
|
finally
|
|
{
|
|
_isStreaming = false;
|
|
StateHasChanged();
|
|
}
|
|
}
|
|
|
|
private async Task HandleActionClick(string action)
|
|
{
|
|
if (action.Contains("quiz", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
await QuizState.RequestQuiz(ContextBlockId);
|
|
}
|
|
|
|
if (OnActionTriggered.HasDelegate)
|
|
{
|
|
await OnActionTriggered.InvokeAsync(action);
|
|
}
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
_streamCts?.Cancel();
|
|
_streamCts?.Dispose();
|
|
}
|
|
}
|