feat(ui/arch): Optimize Graph Dynamics, Immersive Reader, and Core Stability (#19)

This PR introduces a major optimization of graph dynamics, immersive reading experience, and architectural stabilization.

### 🚀 Key Improvements

- **Knowledge Graph (Fix #16)**:
  - Implemented smooth D3.js transitions using the General Update Pattern.
  - Added "Neon Flash" entry animations and dynamic node dimming for better focus.
- **Immersive Reader (Fix #12)**:
  - Standardized centered layout (`max-width: 800px`) with **Merriweather** typography.
  - Optimized line-height and letter-spacing for premium readability.
- **Technical Code Blocks (Fix #20)**:
  - High-contrast dark containers for code snippets.
  - **JetBrains Mono** integration and neon-accented scrollbars.
- **Architectural Stabilization**:
  - Enforced a strict **'no async void'** policy in UI services using `Func<Task>`.
  - Resolved WASM runtime DI errors by implementing dummy service proxies for server-side dependencies.
  - Replaced generic 'Not Found' message with a branded Nexus preloader.

Fixes #7, Fixes #12, Fixes #16, Fixes #20.

Reviewed-on: #19
Co-authored-by: Marek Jasiński <jasins.marek@gmail.com>
Co-committed-by: Marek Jasiński <jasins.marek@gmail.com>
This commit was merged in pull request #19.
This commit is contained in:
2026-05-08 18:16:09 +00:00
committed by Marek Jaisński
parent 775fb73fa9
commit 55cc3ae10d
38 changed files with 442 additions and 179 deletions
@@ -6,7 +6,7 @@ public sealed class FocusModeService : IFocusModeService
{
private readonly IJSRuntime _jsRuntime;
public bool IsFocusModeActive { get; private set; }
public event Action? OnFocusModeChanged;
public event Func<Task>? OnFocusModeChanged;
public FocusModeService(IJSRuntime jsRuntime)
{
@@ -21,7 +21,7 @@ public sealed class FocusModeService : IFocusModeService
if (value == "true" && !IsFocusModeActive)
{
IsFocusModeActive = true;
OnFocusModeChanged?.Invoke();
if (OnFocusModeChanged != null) await OnFocusModeChanged();
}
}
catch
@@ -33,7 +33,7 @@ public sealed class FocusModeService : IFocusModeService
public async Task ToggleAsync()
{
IsFocusModeActive = !IsFocusModeActive;
OnFocusModeChanged?.Invoke();
if (OnFocusModeChanged != null) await OnFocusModeChanged();
try
{
@@ -3,7 +3,7 @@ namespace NexusReader.UI.Shared.Services;
public interface IFocusModeService
{
bool IsFocusModeActive { get; }
event Action? OnFocusModeChanged;
event Func<Task>? OnFocusModeChanged;
Task InitializeAsync();
Task ToggleAsync();
}
@@ -8,12 +8,12 @@ public interface IKnowledgeGraphService
string? ActiveNodeId { get; }
bool IsLoading { get; }
event Action? OnGraphUpdated;
event Action<string>? OnActiveNodeChanged;
event Action<bool>? OnLoadingChanged;
event Func<Task>? OnGraphUpdated;
event Func<string, Task>? OnActiveNodeChanged;
event Func<bool, Task>? OnLoadingChanged;
void UpdateGraph(GraphDataDto newData);
void SetActiveNode(string nodeId);
void SetLoading(bool isLoading);
void Clear();
Task UpdateGraph(GraphDataDto newData);
Task SetActiveNode(string nodeId);
Task SetLoading(bool isLoading);
Task Clear();
}
@@ -9,11 +9,11 @@ public interface IQuizStateService
bool IsHydrating { get; }
bool HasNewQuiz { get; }
event Action<string>? OnQuizRequested;
event Action? OnQuizUpdated;
event Func<string, Task>? OnQuizRequested;
event Func<Task>? OnQuizUpdated;
void RequestQuiz(string blockId);
void SetQuiz(string? blockId, QuizDto quiz);
void SetHydrating(bool hydrating);
void MarkQuizAsSeen();
Task RequestQuiz(string blockId);
Task SetQuiz(string? blockId, QuizDto? quiz);
Task SetHydrating(bool hydrating);
Task MarkQuizAsSeen();
}
@@ -2,15 +2,15 @@ namespace NexusReader.UI.Shared.Services;
public interface IReaderInteractionService
{
event Action<string>? OnNodeSelected;
event Action<string>? OnScrollToBlockRequested;
event Action<string>? OnHighlightBlockRequested;
event Action<string, string, SelectionCoordinates>? OnTextSelected;
event Func<string, Task>? OnNodeSelected;
event Func<string, Task>? OnScrollToBlockRequested;
event Func<string, Task>? OnHighlightBlockRequested;
event Func<string, string, SelectionCoordinates, Task>? OnTextSelected;
void NotifyNodeSelected(string nodeId);
void RequestScrollToBlock(string blockId);
void RequestHighlightBlock(string blockId);
void NotifyTextSelected(string text, string blockId, SelectionCoordinates coords);
Task NotifyNodeSelected(string nodeId);
Task RequestScrollToBlock(string blockId);
Task RequestHighlightBlock(string blockId);
Task NotifyTextSelected(string text, string blockId, SelectionCoordinates coords);
}
public record SelectionCoordinates(double Top, double Left, double Width);
@@ -6,6 +6,6 @@ public interface ISyncService
{
Task<Result> InitializeAsync();
Task<Result> UpdateProgressAsync(string pageId);
event Action<string, DateTime> OnProgressReceived;
event Func<string, DateTime, Task> OnProgressReceived;
Task DisposeAsync();
}
@@ -3,6 +3,6 @@ namespace NexusReader.UI.Shared.Services;
public interface IThemeService
{
bool IsLightMode { get; }
event Action? OnThemeChanged;
void ToggleTheme();
event Func<Task>? OnThemeChanged;
Task ToggleTheme();
}
@@ -36,10 +36,10 @@ public sealed partial class KnowledgeCoordinator : IDisposable
_interactionService.OnNodeSelected += HandleNodeSelected;
}
private void HandleNodeSelected(string nodeId)
private async Task HandleNodeSelected(string nodeId)
{
_interactionService.RequestScrollToBlock(nodeId);
_interactionService.RequestHighlightBlock(nodeId);
await _interactionService.RequestScrollToBlock(nodeId);
await _interactionService.RequestHighlightBlock(nodeId);
}
public async Task ProcessFullPageAsync(string fullContent, string tenantId = "global")
@@ -48,8 +48,8 @@ public sealed partial class KnowledgeCoordinator : IDisposable
LogGeneratingGraph(tenantId);
_graphService.Clear();
_graphService.SetLoading(true);
await _graphService.Clear();
await _graphService.SetLoading(true);
try
{
@@ -59,7 +59,7 @@ public sealed partial class KnowledgeCoordinator : IDisposable
var packet = result.Value;
if (packet.Graph != null)
{
_graphService.UpdateGraph(packet.Graph);
await _graphService.UpdateGraph(packet.Graph);
OnGraphUpdated?.Invoke(packet.Graph);
await _platformService.VibrateSuccessAsync();
}
@@ -71,10 +71,10 @@ public sealed partial class KnowledgeCoordinator : IDisposable
}
}
public void OnBlockReached(string blockId, string content)
public async Task OnBlockReachedAsync(string blockId, string content)
{
// Only update active node for "TU JESTEŚ" logic, do NOT trigger highlight here
_graphService.SetActiveNode(blockId);
await _graphService.SetActiveNode(blockId);
}
public async Task<KnowledgePacket?> RequestSummaryAndQuizAsync(string content, string tenantId = "global")
@@ -109,9 +109,9 @@ public sealed partial class KnowledgeCoordinator : IDisposable
return null;
}
public void Clear()
public async Task ClearAsync()
{
_graphService.Clear();
await _graphService.Clear();
_quizService.SetQuiz(null, null);
}
@@ -8,36 +8,36 @@ public sealed class KnowledgeGraphService : IKnowledgeGraphService
public string? ActiveNodeId { get; private set; }
public bool IsLoading { get; private set; }
public event Action? OnGraphUpdated;
public event Action<string>? OnActiveNodeChanged;
public event Action<bool>? OnLoadingChanged;
public event Func<Task>? OnGraphUpdated;
public event Func<string, Task>? OnActiveNodeChanged;
public event Func<bool, Task>? OnLoadingChanged;
public void UpdateGraph(GraphDataDto newData)
public async Task UpdateGraph(GraphDataDto newData)
{
CurrentGraphData = newData;
IsLoading = false;
OnLoadingChanged?.Invoke(false);
OnGraphUpdated?.Invoke();
if (OnLoadingChanged != null) await OnLoadingChanged(false);
if (OnGraphUpdated != null) await OnGraphUpdated();
}
public void SetActiveNode(string nodeId)
public async Task SetActiveNode(string nodeId)
{
if (ActiveNodeId == nodeId) return;
ActiveNodeId = nodeId;
OnActiveNodeChanged?.Invoke(nodeId);
if (OnActiveNodeChanged != null) await OnActiveNodeChanged(nodeId);
}
public void SetLoading(bool isLoading)
public async Task SetLoading(bool isLoading)
{
IsLoading = isLoading;
OnLoadingChanged?.Invoke(isLoading);
if (OnLoadingChanged != null) await OnLoadingChanged(isLoading);
}
public void Clear()
public async Task Clear()
{
CurrentGraphData = null;
ActiveNodeId = null;
IsLoading = false;
OnGraphUpdated?.Invoke();
if (OnGraphUpdated != null) await OnGraphUpdated();
}
}
@@ -9,34 +9,34 @@ public sealed class QuizStateService : IQuizStateService
public bool IsHydrating { get; private set; }
public bool HasNewQuiz { get; private set; }
public event Action<string>? OnQuizRequested;
public event Action? OnQuizUpdated;
public event Func<string, Task>? OnQuizRequested;
public event Func<Task>? OnQuizUpdated;
public void RequestQuiz(string blockId)
public async Task RequestQuiz(string blockId)
{
CurrentQuizBlockId = blockId;
OnQuizRequested?.Invoke(blockId);
if (OnQuizRequested != null) await OnQuizRequested(blockId);
}
public void SetQuiz(string? blockId, QuizDto quiz)
public async Task SetQuiz(string? blockId, QuizDto? quiz)
{
CurrentQuizBlockId = blockId;
CurrentQuiz = quiz;
IsHydrating = false;
HasNewQuiz = true;
OnQuizUpdated?.Invoke();
HasNewQuiz = quiz != null;
if (OnQuizUpdated != null) await OnQuizUpdated();
}
public void SetHydrating(bool hydrating)
public async Task SetHydrating(bool hydrating)
{
IsHydrating = hydrating;
OnQuizUpdated?.Invoke();
if (OnQuizUpdated != null) await OnQuizUpdated();
}
public void MarkQuizAsSeen()
public async Task MarkQuizAsSeen()
{
if (!HasNewQuiz) return;
HasNewQuiz = false;
OnQuizUpdated?.Invoke();
if (OnQuizUpdated != null) await OnQuizUpdated();
}
}
@@ -2,28 +2,28 @@ namespace NexusReader.UI.Shared.Services;
public sealed class ReaderInteractionService : IReaderInteractionService
{
public event Action<string>? OnNodeSelected;
public event Action<string>? OnScrollToBlockRequested;
public event Action<string>? OnHighlightBlockRequested;
public event Action<string, string, SelectionCoordinates>? OnTextSelected;
public event Func<string, Task>? OnNodeSelected;
public event Func<string, Task>? OnScrollToBlockRequested;
public event Func<string, Task>? OnHighlightBlockRequested;
public event Func<string, string, SelectionCoordinates, Task>? OnTextSelected;
public void NotifyNodeSelected(string nodeId)
public async Task NotifyNodeSelected(string nodeId)
{
OnNodeSelected?.Invoke(nodeId);
if (OnNodeSelected != null) await OnNodeSelected(nodeId);
}
public void RequestScrollToBlock(string blockId)
public async Task RequestScrollToBlock(string blockId)
{
OnScrollToBlockRequested?.Invoke(blockId);
if (OnScrollToBlockRequested != null) await OnScrollToBlockRequested(blockId);
}
public void RequestHighlightBlock(string blockId)
public async Task RequestHighlightBlock(string blockId)
{
OnHighlightBlockRequested?.Invoke(blockId);
if (OnHighlightBlockRequested != null) await OnHighlightBlockRequested(blockId);
}
public void NotifyTextSelected(string text, string blockId, SelectionCoordinates coords)
public async Task NotifyTextSelected(string text, string blockId, SelectionCoordinates coords)
{
OnTextSelected?.Invoke(text, blockId, coords);
if (OnTextSelected != null) await OnTextSelected(text, blockId, coords);
}
}
@@ -14,7 +14,7 @@ public class SyncService : ISyncService, IAsyncDisposable
private bool _isInitialized;
private CancellationTokenSource? _debounceCts;
public event Action<string, DateTime>? OnProgressReceived;
public event Func<string, DateTime, Task>? OnProgressReceived;
public SyncService(
HttpClient httpClient,
@@ -44,9 +44,9 @@ public class SyncService : ISyncService, IAsyncDisposable
.WithAutomaticReconnect()
.Build();
_hubConnection.On<string, DateTime>("ProgressUpdated", (pageId, timestamp) =>
_hubConnection.On<string, DateTime>("ProgressUpdated", async (pageId, timestamp) =>
{
OnProgressReceived?.Invoke(pageId, timestamp);
if (OnProgressReceived != null) await OnProgressReceived(pageId, timestamp);
});
try
@@ -3,11 +3,11 @@ namespace NexusReader.UI.Shared.Services;
public sealed class ThemeService : IThemeService
{
public bool IsLightMode { get; private set; } = false;
public event Action? OnThemeChanged;
public event Func<Task>? OnThemeChanged;
public void ToggleTheme()
public async Task ToggleTheme()
{
IsLightMode = !IsLightMode;
OnThemeChanged?.Invoke();
if (OnThemeChanged != null) await OnThemeChanged();
}
}