Files
Nexus.Reader/src/NexusReader.UI.Shared/Pages/ConceptsDashboard.razor
T

307 lines
12 KiB
Plaintext

@page "/book/{BookId:guid}/concepts"
@page "/concepts-map"
@using Microsoft.AspNetCore.Authorization
@using NexusReader.UI.Shared.Components.Atoms
@using NexusReader.UI.Shared.Components.Organisms
@using NexusReader.UI.Shared.Services
@using NexusReader.Application.Queries.Graph
@using NexusReader.Application.Queries.Concepts
@using System.Net.Http.Json
@inject HttpClient Http
@inject NavigationManager NavigationManager
@inject IIdentityService IdentityService
@inject ISyncService SyncService
@attribute [Authorize]
@implements IDisposable
<PageTitle>Mapa Pojęć | Nexus Reader</PageTitle>
<div class="concepts-dashboard-container">
@if (_isLoading)
{
<div class="loading-state glass-panel">
<div class="preloader-robot">
<NexusIcon Name="robot" Size="64" Class="neon-pulse" />
<div class="scan-line"></div>
</div>
<p class="loading-text">Inicjalizowanie mapy pojęć...</p>
</div>
}
else if (!string.IsNullOrEmpty(_errorMessage))
{
<div class="error-state glass-panel">
<NexusIcon Name="alert-triangle" Size="48" Class="error-icon" />
<h3>Wystąpił Błąd</h3>
<p>@_errorMessage</p>
<button class="nexus-btn secondary-neon" @onclick="LoadDataAsync">Spróbuj ponownie</button>
</div>
}
else if (!BookId.HasValue || BookId.Value == Guid.Empty || Nodes == null || !Nodes.Any())
{
<div class="empty-dashboard-state glass-panel">
<NexusIcon Name="book-open" Size="64" Class="dim-icon" />
<h2>Brak Aktywnych Książek</h2>
<p>Nie wybrano żadnej książki lub ta książka nie ma jeszcze wygenerowanej mapy pojęć przez Nexus AI.</p>
<a href="/library" class="nexus-btn primary-neon">Przejdź do Biblioteki</a>
</div>
}
else
{
<header class="dashboard-header">
<div class="header-back">
<button class="btn-back" @onclick="GoBackToLibrary">
<NexusIcon Name="arrow-left" Size="16" />
<span>Biblioteka</span>
</button>
</div>
<div class="header-title">
<h1>Mapa Pojęć</h1>
<span class="subtitle">Interaktywna ścieżka rozwoju Twoich postępów nauki</span>
</div>
<div class="header-actions">
<button class="btn-action" @onclick="GoToReader">
<span>Otwórz Czytnik</span>
<NexusIcon Name="book-open" Size="16" />
</button>
</div>
</header>
<div class="concepts-dashboard">
<!-- Left Pane: Interactive Skill Tree -->
<section class="left-pane glass-panel">
<div class="pane-header">
<h3><NexusIcon Name="map" Size="18" /> Ścieżka Rozwoju Wiedzy</h3>
</div>
<div class="pane-content">
<ConceptsMap Nodes="Nodes"
LastReadBlockId="LastReadBlockId"
SelectedNode="SelectedNode"
OnNodeSelected="HandleNodeSelected" />
</div>
</section>
<!-- Right Pane: Reactive Deep-Dive Workspace -->
<section class="right-pane glass-panel">
@if (SelectedNode == null)
{
<div class="workspace-empty">
<div class="empty-glowing-brain">
<NexusIcon Name="cpu" Size="48" Class="neon-pulse" />
</div>
<h4>Wybierz węzeł na mapie</h4>
<p>Kliknij dowolne pojęcie z lewego panelu, aby uruchomić głęboką analizę i prześledzić szczegóły wygenerowane przez sztuczną inteligencję.</p>
</div>
}
else
{
var isSelectedNodeUnlocked = IsUnlocked(SelectedNode.Id);
<div class="workspace-content">
<div class="workspace-header">
<div class="node-meta">
<span class="node-id">@SelectedNode.Id.ToUpper()</span>
@if (isSelectedNodeUnlocked)
{
<span class="badge badge-unlocked">
<NexusIcon Name="check" Size="12" /> Odblokowane
</span>
}
else
{
<span class="badge badge-locked">
<NexusIcon Name="lock" Size="12" /> Zablokowane
</span>
}
</div>
<h2 class="workspace-title">@SelectedNode.Label</h2>
</div>
<div class="workspace-body scrollable-content">
@if (!isSelectedNodeUnlocked)
{
<div class="locked-warning">
<NexusIcon Name="lock" Size="20" Class="lock-warning-icon" />
<div>
<strong>Ten etap jest zablokowany</strong>
<p>Kontynuuj czytanie książki, aby odblokować to pojęcie. Po przeczytaniu rozdziału, postęp zsynchronizuje się automatycznie.</p>
</div>
</div>
}
<div class="metadata-section">
<h4><NexusIcon Name="info" Size="14" /> Opis Pojęcia</h4>
<p class="section-text">@SelectedNode.Description</p>
</div>
<div class="metadata-section">
<h4><NexusIcon Name="file-text" Size="14" /> Podsumowanie AI</h4>
<div class="summary-box">
<p class="section-text">@SelectedNode.Summary</p>
</div>
</div>
@if (SelectedNode.KeyTerms != null && SelectedNode.KeyTerms.Any())
{
<div class="metadata-section">
<h4><NexusIcon Name="list" Size="14" /> Kluczowe Terminy</h4>
<div class="key-terms-grid">
@foreach (var term in SelectedNode.KeyTerms)
{
<span class="term-pill">@term</span>
}
</div>
</div>
}
</div>
<div class="workspace-footer">
<button class="nexus-btn primary-neon w-100" @onclick="GoToSelectedChapter">
<NexusIcon Name="book-open" Size="16" />
<span>Czytaj ten rozdział</span>
</button>
</div>
</div>
}
</section>
</div>
}
</div>
@code {
[Parameter] public Guid? BookId { get; set; }
private List<GraphNodeDto> Nodes { get; set; } = new();
private string LastReadBlockId { get; set; } = string.Empty;
private GraphNodeDto? SelectedNode { get; set; }
private bool _isLoading = true;
private string _errorMessage = string.Empty;
protected override async Task OnInitializedAsync()
{
IdentityService.OnStateInvalidated += HandleStateInvalidatedAsync;
await SyncService.InitializeAsync();
SyncService.OnProgressReceived += HandleProgressReceivedAsync;
await LoadDataAsync();
}
private async Task LoadDataAsync()
{
_isLoading = true;
_errorMessage = string.Empty;
StateHasChanged();
try
{
if (!BookId.HasValue || BookId.Value == Guid.Empty)
{
var profileResult = await IdentityService.GetProfileAsync();
if (profileResult.IsSuccess && profileResult.Value.LastReadBook != null)
{
BookId = profileResult.Value.LastReadBook.Id;
}
}
if (BookId.HasValue && BookId.Value != Guid.Empty)
{
var result = await Http.GetFromJsonAsync<BookConceptsMapResultDto>($"api/book/{BookId}/concepts-map");
if (result != null)
{
Nodes = result.Nodes;
LastReadBlockId = result.LastReadBlockId;
if (Nodes.Any())
{
SelectedNode = Nodes.FirstOrDefault(n => IsUnlocked(n.Id)) ?? Nodes.First();
}
}
else
{
_errorMessage = "Brak odpowiedzi od serwera.";
}
}
}
catch (Exception ex)
{
_errorMessage = $"Błąd podczas pobierania danych: {ex.Message}";
}
finally
{
_isLoading = false;
StateHasChanged();
}
}
private bool IsUnlocked(string nodeId)
{
if (string.IsNullOrEmpty(nodeId)) return false;
var nodeSeq = ParseSegmentNumber(nodeId);
var minNodeSeq = Nodes.Any() ? Nodes.Min(n => ParseSegmentNumber(n.Id)) : 0;
if (nodeSeq == minNodeSeq) return true;
if (string.IsNullOrEmpty(LastReadBlockId)) return false;
var progressSeq = ParseSegmentNumber(LastReadBlockId);
return nodeSeq <= progressSeq;
}
private static int ParseSegmentNumber(string? id)
{
if (string.IsNullOrEmpty(id)) return 0;
var digits = new string(id.Where(char.IsDigit).ToArray());
return int.TryParse(digits, out var val) ? val : 0;
}
private void HandleNodeSelected(GraphNodeDto node)
{
SelectedNode = node;
StateHasChanged();
}
private void GoBackToLibrary()
{
NavigationManager.NavigateTo("/library");
}
private void GoToReader()
{
if (BookId.HasValue)
{
NavigationManager.NavigateTo($"/reader/{BookId.Value}");
}
}
private void GoToSelectedChapter()
{
if (BookId.HasValue && SelectedNode != null)
{
var chapterIndex = ParseSegmentNumber(SelectedNode.Id);
NavigationManager.NavigateTo($"/reader/{BookId.Value}?chapter={chapterIndex}");
}
}
private async Task HandleStateInvalidatedAsync()
{
await InvokeAsync(async () =>
{
await LoadDataAsync();
});
}
private async Task HandleProgressReceivedAsync(string pageId, DateTime timestamp)
{
await InvokeAsync(() =>
{
LastReadBlockId = pageId;
StateHasChanged();
return Task.CompletedTask;
});
}
public void Dispose()
{
IdentityService.OnStateInvalidated -= HandleStateInvalidatedAsync;
SyncService.OnProgressReceived -= HandleProgressReceivedAsync;
}
}