307 lines
12 KiB
Plaintext
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;
|
|
}
|
|
}
|