Files
Nexus.Reader/src/NexusReader.UI.Shared/Pages/Account/Login.razor
T
Antigravity 541e9e1fb5 feat(ai-ux): deduplicate AI queries, handle ServiceUnavailable retries, and optimize reader canvas graph prerendering (#44)
This Pull Request encapsulates all outstanding AI, Blazor InteractiveAuto lifecycle, pgvector, and Firefox authorization/session compatibility fixes.

### Key Accomplishments:
1. **Concurrent Request Deduplication (Option B):** Implemented a thread-safe active task registry in `KnowledgeService` that groups concurrent graph extraction queries for the same content, preventing duplicate AI calls completely.
2. **Resilience Strategy for Downstream Demands:** Extended the `ai-retry` resilience pipeline to automatically intercept and retry on temporary Google API `503 ServiceUnavailable` / `high demand` spikes.
3. **Interactive Graph Generation Guard (Option A):** Prevented server-side prerender-phase graph requests in the reader canvas component.
4. **Firefox Compatibility & Cookie Handler:** Implemented an authentication endpoint and hybrid hidden-form submission flow to solve login, registration, and logout redirections and cookies securely.
5. **Autoscrolling & Graph Exclusions:** Added concept-to-block smooth scrolling, active block badging, and filtered out markdown code blocks from being extracted as nodes.

All unit tests compiled and passed 100% cleanly.

---------

Co-authored-by: Marek Jasiński <jasins.marek@gmail.com>
Reviewed-on: #44
Co-authored-by: Antigravity <antigravity@google.com>
Co-committed-by: Antigravity <antigravity@google.com>
2026-05-18 17:53:36 +00:00

169 lines
6.3 KiB
Plaintext

@page "/account/login"
@layout AuthLayout
@attribute [AllowAnonymous]
@using Microsoft.AspNetCore.Components.Forms
@using NexusReader.UI.Shared.Services
@using NexusReader.UI.Shared.Components.Atoms
@inject IIdentityService IdentityService
@inject NavigationManager NavigationManager
@inject IJSRuntime JS
<div class="login-page-container">
<div class="mesh-bg"></div>
<div class="auth-card">
<div class="auth-header">
<div class="logo-box">
<NexusIcon Name="robot" Size="48" />
</div>
<h1 class="app-name">E-Czytnik <span>AI</span></h1>
<p class="auth-subtitle">Zaloguj się do E-Czytnika AI</p>
</div>
<div class="social-auth">
<button type="button" class="btn-google-auth" @onclick="HandleGoogleLogin">
<img src="https://www.gstatic.com/images/branding/product/1x/gsa_512dp.png" alt="Google"
style="width: 20px !important; height: 20px !important; flex-shrink: 0;" />
<span>Zaloguj się przez Google</span>
</button>
</div>
<div class="auth-divider">
<span>lub</span>
</div>
<EditForm Model="@_loginModel" OnValidSubmit="HandleLogin" class="auth-form">
<DataAnnotationsValidator />
<div class="field-group">
<div class="field-icon">
<NexusIcon Name="mail" Size="18" />
</div>
<InputText id="email" @bind-Value="_loginModel.Email" placeholder="E-mail" class="field-input" />
</div>
<ValidationMessage For="@(() => _loginModel.Email)" class="auth-validation" />
<div class="field-group">
<div class="field-icon">
<NexusIcon Name="lock" Size="18" />
</div>
<InputText id="password" type="@(_showPassword ? "text" : "password")"
@bind-Value="_loginModel.Password" placeholder="Hasło" class="field-input" />
<button type="button" class="toggle-visibility" @onclick="TogglePassword">
<NexusIcon Name="@(_showPassword ? "eye-off" : "eye")" Size="18" />
</button>
</div>
<ValidationMessage For="@(() => _loginModel.Password)" class="auth-validation" />
@if (!string.IsNullOrEmpty(_errorMessage))
{
<div class="auth-error">@_errorMessage</div>
}
<div class="auth-options">
<label class="remember-me">
<InputCheckbox @bind-Value="_loginModel.RememberMe" />
<span>Zapamiętaj mnie</span>
</label>
</div>
<button type="submit" class="btn-submit-auth" disabled="@_isSubmitting">
@if (_isSubmitting)
{
<div class="auth-loader"></div>
}
else
{
<span>Zaloguj się</span>
}
</button>
</EditForm>
<div class="auth-footer">
<a href="/account/forgot-password" class="auth-link">Zapomniałem hasła?</a>
<p class="auth-switch">Nie masz konta? <a href="/account/register">Zarejestruj się</a></p>
</div>
<div class="auth-legal">
Korzystając z usługi, akceptujesz <a href="/terms">Regulamin</a> i <a href="/privacy">Politykę
Prywatności</a>
</div>
</div>
</div>
<form id="nexusLoginForm" method="post" action="/account/login-form" style="display:none">
<input type="hidden" name="email" value="@_loginModel.Email" />
<input type="hidden" name="password" value="@_loginModel.Password" />
<input type="hidden" name="rememberMe" value="@(_loginModel.RememberMe ? "true" : "false")" />
</form>
@code {
[Parameter]
[SupplyParameterFromQuery(Name = "error")]
public string? ErrorCode { get; set; }
private LoginModel _loginModel = new();
private string? _errorMessage;
private bool _isSubmitting;
private bool _showPassword;
protected override void OnInitialized()
{
if (!string.IsNullOrEmpty(ErrorCode))
{
_errorMessage = ErrorCode switch
{
"ExternalLoginFailed" => "Nie udało się zalogować przez Google. Spróbuj ponownie.",
"ProvisioningFailed" => "Wystąpił błąd podczas przygotowywania Twojego konta.",
"UserAlreadyExists" => "Użytkownik o tym adresie e-mail już istnieje. Zaloguj się tradycyjnie hasłem.",
"LockedOut" => "Twoje konto zostało zablokowane. Spróbuj ponownie później.",
"InvalidCredentials" => "Nieprawidłowy e-mail lub hasło.",
_ => "Wystąpił nieoczekiwany błąd podczas logowania."
};
}
}
private async Task HandleLogin()
{
_isSubmitting = true;
_errorMessage = null;
try
{
var result = await IdentityService.LoginAsync(_loginModel.Email, _loginModel.Password, _loginModel.RememberMe);
if (result.IsSuccess)
{
// Trigger hidden form submission via robust JS helper to perform cookie-based sign-in
await JS.InvokeVoidAsync("nexusAuth.submitLoginForm", "nexusLoginForm", _loginModel.Email, _loginModel.Password, _loginModel.RememberMe);
}
else
{
_errorMessage = result.Errors.FirstOrDefault()?.Message ?? "Nieprawidłowy e-mail lub hasło.";
}
}
catch (Exception ex)
{
_errorMessage = $"Wystąpił błąd logowania: {ex.Message}.";
}
finally
{
_isSubmitting = false;
}
}
private void HandleGoogleLogin() => NavigationManager.NavigateTo("identity/login/google", forceLoad: true);
private void TogglePassword() => _showPassword = !_showPassword;
public class LoginModel
{
[System.ComponentModel.DataAnnotations.Required]
[System.ComponentModel.DataAnnotations.EmailAddress]
public string Email { get; set; } = string.Empty;
[System.ComponentModel.DataAnnotations.Required]
public string Password { get; set; } = string.Empty;
public bool RememberMe { get; set; }
}
}