feat: introduce IConceptsMapService abstraction with server and WASM implementations and update knowledge processing flow

This commit is contained in:
2026-05-26 16:48:46 +02:00
parent 4fd66052ea
commit 44c4ad0903
9 changed files with 135 additions and 12 deletions
@@ -0,0 +1,9 @@
using FluentResults;
using NexusReader.Application.Queries.Concepts;
namespace NexusReader.Application.Abstractions.Services;
public interface IConceptsMapService
{
Task<Result<BookConceptsMapResultDto>> GetConceptsMapAsync(Guid bookId);
}
@@ -103,7 +103,7 @@
_isInteractive = true; _isInteractive = true;
if (ViewModel != null) if (ViewModel != null)
{ {
await Coordinator.ProcessFullPageAsync(GetFullPageContent()); await Coordinator.ProcessFullPageAsync(GetFullPageContent(), ebookId: ViewModel.EbookId);
} }
} }
@@ -246,7 +246,7 @@
if (_isInteractive) if (_isInteractive)
{ {
await Coordinator.ProcessFullPageAsync(GetFullPageContent()); await Coordinator.ProcessFullPageAsync(GetFullPageContent(), ebookId: ViewModel.EbookId);
} }
} }
else else
@@ -7,7 +7,8 @@
@using NexusReader.Application.Queries.Graph @using NexusReader.Application.Queries.Graph
@using NexusReader.Application.Queries.Concepts @using NexusReader.Application.Queries.Concepts
@using System.Net.Http.Json @using System.Net.Http.Json
@inject HttpClient Http @using NexusReader.Application.Abstractions.Services
@inject IConceptsMapService ConceptsMapService
@inject NavigationManager NavigationManager @inject NavigationManager NavigationManager
@inject IIdentityService IdentityService @inject IIdentityService IdentityService
@inject ISyncService SyncService @inject ISyncService SyncService
@@ -203,11 +204,11 @@
if (BookId.HasValue && BookId.Value != Guid.Empty) if (BookId.HasValue && BookId.Value != Guid.Empty)
{ {
var result = await Http.GetFromJsonAsync<BookConceptsMapResultDto>($"api/book/{BookId}/concepts-map"); var result = await ConceptsMapService.GetConceptsMapAsync(BookId.Value);
if (result != null) if (result.IsSuccess)
{ {
Nodes = result.Nodes; Nodes = result.Value.Nodes;
LastReadBlockId = result.LastReadBlockId; LastReadBlockId = result.Value.LastReadBlockId;
if (Nodes.Any()) if (Nodes.Any())
{ {
@@ -216,7 +217,7 @@
} }
else else
{ {
_errorMessage = "Brak odpowiedzi od serwera."; _errorMessage = result.Errors.FirstOrDefault()?.Message ?? "Brak odpowiedzi od serwera.";
} }
} }
} }
@@ -75,7 +75,7 @@ public sealed partial class KnowledgeCoordinator : IDisposable
} }
} }
public async Task ProcessFullPageAsync(string fullContent, string tenantId = "global") public async Task ProcessFullPageAsync(string fullContent, string tenantId = "global", Guid? ebookId = null)
{ {
if (string.IsNullOrWhiteSpace(fullContent)) return; if (string.IsNullOrWhiteSpace(fullContent)) return;
@@ -87,7 +87,7 @@ public sealed partial class KnowledgeCoordinator : IDisposable
try try
{ {
var result = await _knowledgeService.GetGraphDataAsync(fullContent, tenantId); var result = await _knowledgeService.GetGraphDataAsync(fullContent, tenantId, ebookId);
if (result.IsSuccess) if (result.IsSuccess)
{ {
var packet = result.Value; var packet = result.Value;
+1
View File
@@ -36,6 +36,7 @@ builder.Services.AddCascadingAuthenticationState();
// AI & Content Services // AI & Content Services
builder.Services.AddScoped<IKnowledgeService, WasmKnowledgeService>(); builder.Services.AddScoped<IKnowledgeService, WasmKnowledgeService>();
builder.Services.AddScoped<IConceptsMapService, WasmConceptsMapService>();
builder.Services.AddTransient<NexusReader.Web.Client.Handlers.AuthenticationHeaderHandler>(); builder.Services.AddTransient<NexusReader.Web.Client.Handlers.AuthenticationHeaderHandler>();
builder.Services.AddHttpClient("NexusAPI", client => builder.Services.AddHttpClient("NexusAPI", client =>
@@ -0,0 +1,36 @@
using System.Net.Http.Json;
using FluentResults;
using NexusReader.Application.Abstractions.Services;
using NexusReader.Application.Queries.Concepts;
namespace NexusReader.Web.Client.Services;
public class WasmConceptsMapService : IConceptsMapService
{
private readonly HttpClient _httpClient;
public WasmConceptsMapService(HttpClient httpClient)
{
_httpClient = httpClient;
}
public async Task<Result<BookConceptsMapResultDto>> GetConceptsMapAsync(Guid bookId)
{
try
{
var response = await _httpClient.GetAsync($"api/book/{bookId}/concepts-map");
if (response.IsSuccessStatusCode)
{
var result = await response.Content.ReadFromJsonAsync<BookConceptsMapResultDto>();
return result != null ? Result.Ok(result) : Result.Fail<BookConceptsMapResultDto>("Błąd deserializacji mapy pojęć.");
}
var errorContent = await response.Content.ReadAsStringAsync();
return Result.Fail<BookConceptsMapResultDto>($"Błąd serwera ({response.StatusCode}): {errorContent}");
}
catch (Exception ex)
{
return Result.Fail<BookConceptsMapResultDto>(new Error("Błąd sieci przy pobieraniu mapy pojęć.").CausedBy(ex));
}
}
}
+6
View File
@@ -74,6 +74,7 @@ builder.Services.AddHttpClient("NexusAPI", (sp, client) =>
builder.Services.AddScoped(sp => sp.GetRequiredService<IHttpClientFactory>().CreateClient("NexusAPI")); builder.Services.AddScoped(sp => sp.GetRequiredService<IHttpClientFactory>().CreateClient("NexusAPI"));
builder.Services.AddScoped<IIdentityService, NexusReader.Web.Services.ServerIdentityService>(); builder.Services.AddScoped<IIdentityService, NexusReader.Web.Services.ServerIdentityService>();
builder.Services.AddScoped<IConceptsMapService, NexusReader.Web.Services.ServerConceptsMapService>();
builder.Services.AddCascadingAuthenticationState(); builder.Services.AddCascadingAuthenticationState();
builder.Services.AddApplication(); builder.Services.AddApplication();
@@ -87,6 +88,11 @@ builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssemblies(
// Authorization Policies // Authorization Policies
builder.Services.AddScoped<IAuthorizationHandler, TokenLimitHandler>(); builder.Services.AddScoped<IAuthorizationHandler, TokenLimitHandler>();
builder.Services.AddAuthorizationBuilder() builder.Services.AddAuthorizationBuilder()
.SetDefaultPolicy(new Microsoft.AspNetCore.Authorization.AuthorizationPolicyBuilder(
IdentityConstants.ApplicationScheme,
IdentityConstants.BearerScheme)
.RequireAuthenticatedUser()
.Build())
.AddPolicy("ProUser", policy => policy.RequireClaim("Plan", SubscriptionPlan.ProName, SubscriptionPlan.EnterpriseName)) .AddPolicy("ProUser", policy => policy.RequireClaim("Plan", SubscriptionPlan.ProName, SubscriptionPlan.EnterpriseName))
.AddPolicy("HasAvailableTokens", policy => policy.AddRequirements(new TokenLimitRequirement())); .AddPolicy("HasAvailableTokens", policy => policy.AddRequirements(new TokenLimitRequirement()));
@@ -0,0 +1,59 @@
using System.Security.Claims;
using FluentResults;
using MediatR;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Http;
using NexusReader.Application.Abstractions.Services;
using NexusReader.Application.Queries.Concepts;
namespace NexusReader.Web.Services;
public class ServerConceptsMapService : IConceptsMapService
{
private readonly IMediator _mediator;
private readonly AuthenticationStateProvider _authStateProvider;
private readonly IHttpContextAccessor _httpContextAccessor;
public ServerConceptsMapService(
IMediator mediator,
AuthenticationStateProvider authStateProvider,
IHttpContextAccessor httpContextAccessor)
{
_mediator = mediator;
_authStateProvider = authStateProvider;
_httpContextAccessor = httpContextAccessor;
}
public async Task<Result<BookConceptsMapResultDto>> GetConceptsMapAsync(Guid bookId)
{
try
{
var authState = await _authStateProvider.GetAuthenticationStateAsync();
var user = authState.User;
if (user == null || !user.Identity?.IsAuthenticated == true)
{
user = _httpContextAccessor.HttpContext?.User;
}
if (user == null || !user.Identity?.IsAuthenticated == true)
{
return Result.Fail<BookConceptsMapResultDto>("Użytkownik nie jest uwierzytelniony.");
}
var userId = user.FindFirstValue(ClaimTypes.NameIdentifier);
var tenantId = user.FindFirstValue("TenantId") ?? "global";
if (string.IsNullOrEmpty(userId))
{
return Result.Fail<BookConceptsMapResultDto>("Nie znaleziono identyfikatora użytkownika.");
}
return await _mediator.Send(new GetBookConceptsMapQuery(bookId, userId, tenantId));
}
catch (Exception ex)
{
return Result.Fail<BookConceptsMapResultDto>(new Error("Błąd pobierania mapy pojęć na serwerze.").CausedBy(ex));
}
}
}
@@ -9,6 +9,7 @@ using NexusReader.Application.Queries.User;
using MediatR; using MediatR;
using NexusReader.Application.Constants; using NexusReader.Application.Constants;
using NexusReader.Application.Abstractions.Services; using NexusReader.Application.Abstractions.Services;
using Microsoft.AspNetCore.Components.Authorization;
namespace NexusReader.Web.Services; namespace NexusReader.Web.Services;
@@ -19,6 +20,7 @@ public class ServerIdentityService : IIdentityService
private readonly IHttpContextAccessor _httpContextAccessor; private readonly IHttpContextAccessor _httpContextAccessor;
private readonly IMediator _mediator; private readonly IMediator _mediator;
private readonly INativeStorageService _storageService; private readonly INativeStorageService _storageService;
private readonly AuthenticationStateProvider _authStateProvider;
public event Func<Task>? OnStateInvalidated; public event Func<Task>? OnStateInvalidated;
@@ -27,13 +29,15 @@ public class ServerIdentityService : IIdentityService
SignInManager<NexusUser> signInManager, SignInManager<NexusUser> signInManager,
IHttpContextAccessor httpContextAccessor, IHttpContextAccessor httpContextAccessor,
IMediator mediator, IMediator mediator,
INativeStorageService storageService) INativeStorageService storageService,
AuthenticationStateProvider authStateProvider)
{ {
_userManager = userManager; _userManager = userManager;
_signInManager = signInManager; _signInManager = signInManager;
_httpContextAccessor = httpContextAccessor; _httpContextAccessor = httpContextAccessor;
_mediator = mediator; _mediator = mediator;
_storageService = storageService; _storageService = storageService;
_authStateProvider = authStateProvider;
} }
public async Task<Result> LoginAsync(string email, string password, bool rememberMe = false) public async Task<Result> LoginAsync(string email, string password, bool rememberMe = false)
@@ -107,7 +111,14 @@ public class ServerIdentityService : IIdentityService
public async Task<Result<UserProfileDto>> GetProfileAsync() public async Task<Result<UserProfileDto>> GetProfileAsync()
{ {
var user = _httpContextAccessor.HttpContext?.User; var authState = await _authStateProvider.GetAuthenticationStateAsync();
var user = authState.User;
if (user == null || !user.Identity?.IsAuthenticated == true)
{
user = _httpContextAccessor.HttpContext?.User;
}
if (user == null || !user.Identity?.IsAuthenticated == true) return Result.Fail("Not authenticated."); if (user == null || !user.Identity?.IsAuthenticated == true) return Result.Fail("Not authenticated.");
var userId = user.FindFirstValue(ClaimTypes.NameIdentifier); var userId = user.FindFirstValue(ClaimTypes.NameIdentifier);