Files
Nexus.Reader/src/NexusReader.UI.Shared/Layout/ReaderLayout.razor
T

470 lines
21 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
@inherits LayoutComponentBase
@using NexusReader.Application.Abstractions.Services
@using NexusReader.UI.Shared.Services
@using NexusReader.UI.Shared.Models
@using NexusReader.UI.Shared.Components.Molecules
@using NexusReader.UI.Shared.Components.Organisms
@using NexusReader.Application.Queries.Graph
@using Microsoft.Extensions.Logging
@inject IPlatformService PlatformService
@inject IFocusModeService FocusMode
@inject IQuizStateService QuizService
@inject IReaderInteractionService InteractionService
@inject IReaderStateService StateService
@inject IKnowledgeGraphService GraphService
@inject IJSRuntime JS
@inject IIdentityService IdentityService
@inject NavigationManager NavigationManager
@inject Microsoft.Extensions.Logging.ILogger<ReaderLayout> Logger
@implements IAsyncDisposable
<div class="app-container @_platformClass @(FocusMode.IsFocusModeActive ? "focus-mode-active" : "") @($"active-mobile-tab-{_activeMobileTab.ToString().ToLower()}")">
<div class="reader-pane">
<main>
@Body
</main>
<AuthorizeView>
<Authorized>
<ReaderFooter />
</Authorized>
</AuthorizeView>
</div>
<AuthorizeView>
<Authorized>
@if (!_isMobile)
{
<div class="resizer" id="sidebar-resizer"></div>
<div class="intelligence-sidebar">
<IntelligenceToolbar />
<div class="intelligence-content">
<div class="intelligence-header">
<div class="ai-title">
<NexusIcon Name="robot" Size="20"
Class="@($"neon-glow {(QuizService.HasNewQuiz ? "quiz-available" : "")}")" />
<span>Asystent AI</span>
</div>
<button class="close-btn">×</button>
</div>
@if (_activeTab == SidebarTab.Knowledge)
{
<div class="intelligence-scroll-area stacked-layout">
<div class="visual-workspace">
<KnowledgeGraph />
</div>
<div class="contextual-intelligence-panel">
<div class="panel-header">
<NexusIcon Name="brain" Size="18" Class="neon-accent-icon" />
<span class="panel-title">Contextual Intelligence Panel</span>
</div>
<div class="panel-body">
@if (_selectedNode != null)
{
<div class="node-details">
<div class="node-header-section">
<span class="node-group-badge @(_selectedNode.Group.ToLower())">@(_selectedNode.Group.ToUpper())</span>
<h3 class="node-label">@_selectedNode.Label</h3>
</div>
@if (!string.IsNullOrEmpty(_selectedNode.Description))
{
<div class="detail-section">
<p class="node-description">@_selectedNode.Description</p>
</div>
}
@if (!string.IsNullOrEmpty(_selectedNode.Summary))
{
<div class="detail-section summary-section">
<h4 class="section-title neon-sub-header">Podsumowanie</h4>
<p class="node-summary">@_selectedNode.Summary</p>
</div>
}
@if (_selectedNode.KeyTerms != null && _selectedNode.KeyTerms.Any())
{
<div class="detail-section key-terms-section">
<h4 class="section-title neon-sub-header">Kluczowe Pojęcia</h4>
<ul class="key-terms-list">
@foreach (var term in _selectedNode.KeyTerms)
{
<li class="key-term-item">
<span class="term-bullet">•</span>
<span class="term-text">@term</span>
</li>
}
</ul>
</div>
}
</div>
}
else
{
<div class="no-node-selected">
<div class="placeholder-glow"></div>
<p class="placeholder-text">Wybierz węzeł na wykresie, aby wyświetlić szczegóły architektoniczne.</p>
</div>
}
</div>
</div>
</div>
<div class="sidebar-footer">
<button class="open-quiz-btn neon-glow-btn @(QuizService.HasNewQuiz ? "quiz-pulse-btn" : "")" @onclick="() => SetActiveTab(SidebarTab.Quiz)">
<NexusIcon Name="quiz" Size="18" />
<span>OPEN KNOWLEDGE QUIZ</span>
</button>
</div>
}
else
{
<div class="intelligence-scroll-area quiz-layout">
<div class="quiz-nav">
<button class="back-to-graph-btn" @onclick="() => SetActiveTab(SidebarTab.Knowledge)">
<NexusIcon Name="arrow-left" Size="16" />
<span>← Powrót do wykresu</span>
</button>
</div>
<KnowledgeCheck />
</div>
}
</div>
</div>
}
else
{
<!-- Mobile full-bleed containers mapped to bottom tab navigation -->
<div class="nexus-mobile-reader-tabs">
<!-- Tab 2: Graph -->
<div class="nexus-mobile-tab-content graph-tab @(_activeMobileTab == MobileReaderTab.Graph ? "active" : "")">
<KnowledgeGraph />
</div>
<!-- Tab 3: Concepts/Quiz -->
<div class="nexus-mobile-tab-content insight-tab @(_activeMobileTab == MobileReaderTab.Concepts ? "active" : "")">
<div class="mobile-insight-container">
<div class="mobile-insight-header">
<div class="mobile-insight-nav">
<button class="mobile-insight-nav-btn @(_activeTab == SidebarTab.Knowledge ? "active" : "")" @onclick="() => SetActiveTab(SidebarTab.Knowledge)">
<NexusIcon Name="brain" Size="16" />
<span>Podgląd pojęcia</span>
</button>
<button class="mobile-insight-nav-btn quiz-btn @(_activeTab == SidebarTab.Quiz ? "active" : "") @(QuizService.HasNewQuiz ? "quiz-pulse" : "")" @onclick="() => SetActiveTab(SidebarTab.Quiz)">
<NexusIcon Name="quiz" Size="16" />
<span>Quiz wiedzy</span>
</button>
</div>
</div>
<div class="mobile-insight-body">
@if (_activeTab == SidebarTab.Knowledge)
{
<div class="contextual-intelligence-panel">
<div class="panel-body">
@if (_selectedNode != null)
{
<div class="node-details">
<div class="node-header-section">
<span class="node-group-badge @(_selectedNode.Group.ToLower())">@(_selectedNode.Group.ToUpper())</span>
<h3 class="node-label">@_selectedNode.Label</h3>
</div>
@if (!string.IsNullOrEmpty(_selectedNode.Description))
{
<div class="detail-section">
<p class="node-description">@_selectedNode.Description</p>
</div>
}
@if (!string.IsNullOrEmpty(_selectedNode.Summary))
{
<div class="detail-section summary-section">
<h4 class="section-title neon-sub-header">Podsumowanie</h4>
<p class="node-summary">@_selectedNode.Summary</p>
</div>
}
@if (_selectedNode.KeyTerms != null && _selectedNode.KeyTerms.Any())
{
<div class="detail-section key-terms-section">
<h4 class="section-title neon-sub-header">Kluczowe Pojęcia</h4>
<ul class="key-terms-list">
@foreach (var term in _selectedNode.KeyTerms)
{
<li class="key-term-item">
<span class="term-bullet">•</span>
<span class="term-text">@term</span>
</li>
}
</ul>
</div>
}
</div>
}
else
{
<div class="no-node-selected">
<div class="placeholder-glow"></div>
<p class="placeholder-text">Wybierz pojęcie na wykresie, aby wyświetlić jego podsumowanie.</p>
</div>
}
</div>
</div>
}
else
{
<div class="mobile-quiz-wrapper">
<KnowledgeCheck />
</div>
}
</div>
</div>
</div>
</div>
<MobileReaderToolbar
ScrollPercentage="@_scrollPercentage"
ActiveTab="@_activeMobileTab"
OnTabChanged="SetMobileTab"
OnAssistantClick="OpenAssistant"
Checkpoints="@StateService.CurrentCheckpoints" />
<GlobalIntelligence IsOpen="@_isAssistantOpen" OnClose="CloseAssistant" TenantId="@context.User.FindFirst("TenantId")?.Value" />
}
</Authorized>
<Authorizing>
<div class="app-preloader">
<div class="preloader-spinner"></div>
<div class="preloader-text">Weryfikacja...</div>
</div>
</Authorizing>
</AuthorizeView>
</div>
<div id="blazor-error-ui" data-nosnippet>
An unhandled error has occurred.
<a href="." class="reload">Reload</a>
<span class="dismiss">🗙</span>
</div>
@code {
private enum SidebarTab
{
Knowledge,
Quiz
}
private SidebarTab _activeTab = SidebarTab.Knowledge;
private string? _selectedNodeId;
private GraphNodeDto? _selectedNode;
private string _platformClass = "platform-desktop";
private bool _isMobile = false;
private DotNetObjectReference<ReaderLayout>? _selfReference;
private IJSObjectReference? _viewportModule;
private bool _isAssistantOpen;
private int _scrollPercentage
{
get => StateService.CurrentScrollPercentage;
set => StateService.CurrentScrollPercentage = value;
}
private MobileReaderTab _activeMobileTab
{
get => StateService.ActiveTab;
set => StateService.ActiveTab = value;
}
protected override void OnInitialized()
{
FocusMode.OnFocusModeChanged += HandleUpdate;
QuizService.OnQuizUpdated += HandleUpdate;
QuizService.OnQuizRequested += HandleQuizRequestedAsync;
InteractionService.OnNodeSelected += HandleNodeSelectedAsync;
InteractionService.OnAssistantRequested += HandleAssistantRequestedAsync;
InteractionService.OnScrollPercentChanged += HandleScrollPercentChanged;
GraphService.OnGraphUpdated += HandleGraphUpdatedAsync;
var context = PlatformService.GetDeviceContext();
if (context.IsSuccess)
{
_isMobile = context.Value.DeviceType switch
{
DeviceType.Phone or DeviceType.Tablet => true,
_ => false
};
_platformClass = _isMobile ? "platform-mobile" : "platform-desktop";
}
}
private void SetActiveTab(SidebarTab tab)
{
_activeTab = tab;
StateHasChanged();
}
private void SetMobileTab(MobileReaderTab tab)
{
_activeMobileTab = tab;
StateHasChanged();
}
private void OpenAssistant()
{
_isAssistantOpen = true;
StateHasChanged();
}
private void CloseAssistant()
{
_isAssistantOpen = false;
StateHasChanged();
}
private async Task HandleScrollPercentChanged(int percent)
{
_scrollPercentage = percent;
await InvokeAsync(StateHasChanged);
}
private async Task HandleQuizRequestedAsync(string blockId)
{
_activeTab = SidebarTab.Quiz;
if (_isMobile)
{
_activeMobileTab = MobileReaderTab.Concepts;
}
await InvokeAsync(StateHasChanged);
}
private async Task HandleAssistantRequestedAsync()
{
if (_isMobile)
{
OpenAssistant();
}
else
{
_activeMobileTab = MobileReaderTab.Concepts;
_activeTab = SidebarTab.Quiz;
}
await InvokeAsync(StateHasChanged);
}
private async Task HandleNodeSelectedAsync(string nodeId)
{
_selectedNodeId = nodeId;
if (GraphService.CurrentGraphData != null)
{
_selectedNode = GraphService.CurrentGraphData.Nodes.FirstOrDefault(n => n.Id == nodeId);
}
if (_isMobile)
{
_activeMobileTab = MobileReaderTab.Concepts;
_activeTab = SidebarTab.Knowledge;
}
await InvokeAsync(StateHasChanged);
}
private async Task HandleGraphUpdatedAsync()
{
_selectedNodeId = null;
_selectedNode = null;
await InvokeAsync(StateHasChanged);
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
try
{
var module = await JS.InvokeAsync<IJSObjectReference>("import", "./_content/NexusReader.UI.Shared/js/layoutResizer.js");
await module.InvokeVoidAsync("initResizer", ".app-container", "#sidebar-resizer", "--sidebar-width");
}
catch (Exception ex)
{
Logger.LogError(ex, "Failed to initialize layout resizer JS module.");
}
try
{
_viewportModule = await JS.InvokeAsync<IJSObjectReference>("import", "./_content/NexusReader.UI.Shared/js/viewport.js");
await InitViewportDetectionAsync();
}
catch (Exception ex)
{
Logger.LogError(ex, "Failed to import viewport utilities JS module.");
}
}
}
private async Task InitViewportDetectionAsync()
{
if (_viewportModule == null) return;
try
{
_selfReference = DotNetObjectReference.Create(this);
var isMobileViewport = await _viewportModule.InvokeAsync<bool>("isMobileViewport");
await OnViewportChanged(isMobileViewport);
await _viewportModule.InvokeVoidAsync("registerViewportObserver", _selfReference);
}
catch (Exception ex)
{
Logger.LogWarning(ex, "Failed to initialize viewport detection.");
}
}
[JSInvokable]
public async Task OnViewportChanged(bool isMobile)
{
if (_isMobile != isMobile)
{
_isMobile = isMobile;
_platformClass = _isMobile ? "platform-mobile" : "platform-desktop";
await InvokeAsync(StateHasChanged);
}
}
private Task HandleUpdate() => InvokeAsync(StateHasChanged);
public async ValueTask DisposeAsync()
{
FocusMode.OnFocusModeChanged -= HandleUpdate;
QuizService.OnQuizUpdated -= HandleUpdate;
QuizService.OnQuizRequested -= HandleQuizRequestedAsync;
InteractionService.OnNodeSelected -= HandleNodeSelectedAsync;
InteractionService.OnAssistantRequested -= HandleAssistantRequestedAsync;
InteractionService.OnScrollPercentChanged -= HandleScrollPercentChanged;
GraphService.OnGraphUpdated -= HandleGraphUpdatedAsync;
try
{
if (_viewportModule != null)
{
if (_selfReference != null)
{
await _viewportModule.InvokeVoidAsync("unregisterViewportObserver", _selfReference);
}
await _viewportModule.DisposeAsync();
}
}
catch (Exception ex)
{
Logger.LogDebug(ex, "Teardown of viewport observer module failed during component disposal.");
}
_selfReference?.Dispose();
}
}