bcd5daa3a0
Refactored Pulpit, Katalog, Moje, Konto screens to a unified, premium Modern Deep Dark style. Resolves #73. --------- Co-authored-by: Marek Jasiński <jasins.marek@gmail.com> Reviewed-on: #74 Co-authored-by: Antigravity <antigravity@google.com> Co-committed-by: Antigravity <antigravity@google.com>
305 lines
12 KiB
Plaintext
305 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
|
|
@using NexusReader.Application.Abstractions.Services
|
|
@using NexusReader.Application.Utilities
|
|
@inject IConceptsMapService ConceptsMapService
|
|
@inject NavigationManager NavigationManager
|
|
@inject IIdentityService IdentityService
|
|
@inject ISyncService SyncService
|
|
@attribute [Authorize]
|
|
@implements IAsyncDisposable
|
|
|
|
<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="btn-nexus btn-nexus-secondary" @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="/my-books" class="btn-nexus btn-nexus-primary">Przejdź do Moich Książek</a>
|
|
</div>
|
|
}
|
|
else
|
|
{
|
|
<header class="dashboard-header">
|
|
<div class="header-back">
|
|
<button class="btn-nexus btn-nexus-secondary btn-back" @onclick="GoBackToLibrary">
|
|
<NexusIcon Name="arrow-left" Size="16" />
|
|
<span>Moje Książki</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-nexus btn-nexus-primary 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="btn-nexus btn-nexus-primary 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 ConceptsMapService.GetConceptsMapAsync(BookId.Value);
|
|
if (result.IsSuccess)
|
|
{
|
|
Nodes = result.Value.Nodes;
|
|
LastReadBlockId = result.Value.LastReadBlockId;
|
|
|
|
if (Nodes.Any())
|
|
{
|
|
SelectedNode = Nodes.FirstOrDefault(n => IsUnlocked(n.Id)) ?? Nodes.First();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
_errorMessage = result.Errors.FirstOrDefault()?.Message ?? "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 = SegmentIdParser.Parse(nodeId);
|
|
|
|
var minNodeSeq = Nodes.Any() ? Nodes.Min(n => SegmentIdParser.Parse(n.Id)) : 0;
|
|
if (nodeSeq == minNodeSeq) return true;
|
|
|
|
if (string.IsNullOrEmpty(LastReadBlockId)) return false;
|
|
|
|
var progressSeq = SegmentIdParser.Parse(LastReadBlockId);
|
|
return nodeSeq <= progressSeq;
|
|
}
|
|
|
|
|
|
|
|
private void HandleNodeSelected(GraphNodeDto node)
|
|
{
|
|
SelectedNode = node;
|
|
StateHasChanged();
|
|
}
|
|
|
|
private void GoBackToLibrary()
|
|
{
|
|
NavigationManager.NavigateTo("/my-books");
|
|
}
|
|
|
|
private void GoToReader()
|
|
{
|
|
if (BookId.HasValue)
|
|
{
|
|
NavigationManager.NavigateTo($"/reader/{BookId.Value}");
|
|
}
|
|
}
|
|
|
|
private void GoToSelectedChapter()
|
|
{
|
|
if (BookId.HasValue && SelectedNode != null)
|
|
{
|
|
var chapterIndex = SegmentIdParser.Parse(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 ValueTask DisposeAsync()
|
|
{
|
|
IdentityService.OnStateInvalidated -= HandleStateInvalidatedAsync;
|
|
SyncService.OnProgressReceived -= HandleProgressReceivedAsync;
|
|
return ValueTask.CompletedTask;
|
|
}
|
|
}
|