feat(ui/graph): optimize graph dynamics, immersive reader, and code blocks
- Fix #16: Implement D3.js transitions and active state badges for Knowledge Graph. - Fix #12: Implement Immersive Reader Layout with Merriweather typography. - Fix #20: Professional code block styling with high contrast and monospace fonts. - Resolve DI runtime error in WASM by adding dummy services. - Replace generic 'Not Found' message with Nexus preloader. - Enforce 'no async void' architecture across UI services.
This commit is contained in:
@@ -29,9 +29,12 @@ This skill defines the architectural guardrails for the NexusReader project to e
|
|||||||
- Use `FluentResults` (`Result<T>`) for all Application services and handlers.
|
- Use `FluentResults` (`Result<T>`) for all Application services and handlers.
|
||||||
- Avoid throwing exceptions for expected business failures; use `Result.Fail()`.
|
- Avoid throwing exceptions for expected business failures; use `Result.Fail()`.
|
||||||
|
|
||||||
### 4. MediatR Patterns
|
|
||||||
- **Queries**: Read-only operations. Should return `Result<T>`. Use `AsNoTracking()` in EF Core.
|
|
||||||
- **Commands**: State-changing operations. Should return `Result` or `Result<T>`.
|
- **Commands**: State-changing operations. Should return `Result` or `Result<T>`.
|
||||||
|
+
|
||||||
|
+### 5. Async Operations (Zero Tolerance for `async void`)
|
||||||
|
+- All asynchronous operations MUST return `Task` or `ValueTask`.
|
||||||
|
+- Event handlers MUST use `Func<Task>` or async-compatible patterns.
|
||||||
|
+- UI components MUST await all service calls and use `InvokeAsync(StateHasChanged)` for state updates within async contexts.
|
||||||
|
|
||||||
## Audit Scripts
|
## Audit Scripts
|
||||||
- [ArchCheck.sh](scripts/arch_check.sh): A shell script to scan for illegal cross-layer imports.
|
- [ArchCheck.sh](scripts/arch_check.sh): A shell script to scan for illegal cross-layer imports.
|
||||||
|
|||||||
@@ -8,4 +8,7 @@ description: D3.js standards for Knowledge Graph
|
|||||||
- **JS Interop:** Use ES6 modules and `IJSObjectReference`.
|
- **JS Interop:** Use ES6 modules and `IJSObjectReference`.
|
||||||
- **Responsiveness:** SVG must use `viewBox` for fluid portrait scaling.
|
- **Responsiveness:** SVG must use `viewBox` for fluid portrait scaling.
|
||||||
- **Visuals:** Use CSS variables (`--nexus-neon`) for node styling.
|
- **Visuals:** Use CSS variables (`--nexus-neon`) for node styling.
|
||||||
|
- **Transitions:** Enforce smooth 500ms transitions using the D3.js General Update Pattern (`.join()`).
|
||||||
|
- **Animations:** Implement "Neon Flash" entry animations for newly discovered knowledge nodes.
|
||||||
|
- **Contextual Highlight:** Support node/link dimming to emphasize the current reading context.
|
||||||
- **Events:** JS emits events (like `nodeClicked`) caught by Blazor via `DotNetObjectReference`.
|
- **Events:** JS emits events (like `nodeClicked`) caught by Blazor via `DotNetObjectReference`.
|
||||||
@@ -22,7 +22,7 @@ description: Design System & Component rules for Blazor
|
|||||||
- Light Mode: `--nexus-bg` (`#f8f9fa`), `--nexus-card` (`#ffffff`).
|
- Light Mode: `--nexus-bg` (`#f8f9fa`), `--nexus-card` (`#ffffff`).
|
||||||
- **Typography:**
|
- **Typography:**
|
||||||
- UI Elements: `Inter` (Sans-Serif) for controls, menus, and labels.
|
- UI Elements: `Inter` (Sans-Serif) for controls, menus, and labels.
|
||||||
- Reading Content: `Merriweather` (Serif) for books and articles to ensure high readability.
|
- Reading Content: `Merriweather` (Serif) with `line-height: 1.65` and `letter-spacing: -0.01em` for high readability.
|
||||||
- **Effects:**
|
- **Effects:**
|
||||||
- Subtle neon glows (`box-shadow: 0 0 15px rgba(0, 255, 153, 0.3)`).
|
- Subtle neon glows (`box-shadow: 0 0 15px rgba(0, 255, 153, 0.3)`).
|
||||||
- Glassmorphism for overlays and modals.
|
- Glassmorphism for overlays and modals.
|
||||||
@@ -30,6 +30,11 @@ description: Design System & Component rules for Blazor
|
|||||||
- **Adaptive Layouts:**
|
- **Adaptive Layouts:**
|
||||||
- Support `.platform-mobile` and `.platform-desktop` context classes.
|
- Support `.platform-mobile` and `.platform-desktop` context classes.
|
||||||
- Handle safe-area insets (`--safe-area-inset-*`) for mobile devices.
|
- Handle safe-area insets (`--safe-area-inset-*`) for mobile devices.
|
||||||
|
- **Immersive Reader (Zen Mode):**
|
||||||
|
- Centered content flow: `max-width: 800px`, `margin: 0 auto`.
|
||||||
|
- Paper-white background: `#F9F9F9` for light mode reader canvas.
|
||||||
|
- Dedicated Scrollbars: Custom styled, thin scrollbars with `--nexus-neon` accents.
|
||||||
|
- Reachability: Large `padding-bottom` (e.g., `15rem`) to ensure comfortable reading of end-of-page content.
|
||||||
|
|
||||||
- **Accessibility (A11y):**
|
- **Accessibility (A11y):**
|
||||||
- Touch Targets: Min `44x44px` on mobile (enforced via CSS variables).
|
- Touch Targets: Min `44x44px` on mobile (enforced via CSS variables).
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ public static class DependencyInjection
|
|||||||
public static IServiceCollection AddApplication(this IServiceCollection services)
|
public static IServiceCollection AddApplication(this IServiceCollection services)
|
||||||
{
|
{
|
||||||
services.AddMapsterConfiguration();
|
services.AddMapsterConfiguration();
|
||||||
|
services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(DependencyInjection).Assembly));
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,7 +42,7 @@
|
|||||||
@code {
|
@code {
|
||||||
protected override void OnInitialized()
|
protected override void OnInitialized()
|
||||||
{
|
{
|
||||||
FocusMode.OnFocusModeChanged += StateHasChanged;
|
FocusMode.OnFocusModeChanged += HandleUpdate;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task HandleClearCache()
|
private async Task HandleClearCache()
|
||||||
@@ -56,8 +56,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Task HandleUpdate() => InvokeAsync(StateHasChanged);
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
FocusMode.OnFocusModeChanged -= StateHasChanged;
|
FocusMode.OnFocusModeChanged -= HandleUpdate;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,12 +55,14 @@
|
|||||||
|
|
||||||
protected override void OnInitialized()
|
protected override void OnInitialized()
|
||||||
{
|
{
|
||||||
QuizService.OnQuizUpdated += () => InvokeAsync(StateHasChanged);
|
QuizService.OnQuizUpdated += HandleUpdate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Task HandleUpdate() => InvokeAsync(StateHasChanged);
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
QuizService.OnQuizUpdated -= StateHasChanged;
|
QuizService.OnQuizUpdated -= HandleUpdate;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task SelectOptionAsync(QuizQuestionDto question, int index)
|
private async Task SelectOptionAsync(QuizQuestionDto question, int index)
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="ai-actions">
|
<div class="ai-actions">
|
||||||
<button class="action-btn neon-border" @onclick="GenerateFullQuiz">Generuj Quiz dla całej strony</button>
|
<button class="action-btn neon-border" @onclick="GenerateFullQuiz">Generuj Quiz dla całej strony</button>
|
||||||
<button class="action-btn ghost" @onclick="Close">Zamknij</button>
|
<button class="action-btn ghost" @onclick="CloseAsync">Zamknij</button>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -39,7 +39,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="ai-actions">
|
<div class="ai-actions">
|
||||||
<button class="action-btn neon-border" @onclick="RequestSummary">Podsumuj zaznaczenie</button>
|
<button class="action-btn neon-border" @onclick="RequestSummary">Podsumuj zaznaczenie</button>
|
||||||
<button class="action-btn ghost" @onclick="Close">Pomiń</button>
|
<button class="action-btn ghost" @onclick="CloseAsync">Pomiń</button>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@@ -89,12 +89,12 @@
|
|||||||
IsLoading = true;
|
IsLoading = true;
|
||||||
await Coordinator.RequestSummaryAndQuizAsync(FullPageContent);
|
await Coordinator.RequestSummaryAndQuizAsync(FullPageContent);
|
||||||
IsLoading = false;
|
IsLoading = false;
|
||||||
Close();
|
await CloseAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Close()
|
private async Task CloseAsync()
|
||||||
{
|
{
|
||||||
Packet = null;
|
Packet = null;
|
||||||
InteractionService.NotifyTextSelected(string.Empty, string.Empty, null!);
|
await InteractionService.NotifyTextSelected(string.Empty, string.Empty, null!);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,7 +46,7 @@
|
|||||||
GraphService.OnLoadingChanged += HandleLoadingChange;
|
GraphService.OnLoadingChanged += HandleLoadingChange;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void HandleGraphUpdate()
|
private async Task HandleGraphUpdate()
|
||||||
{
|
{
|
||||||
if (_module == null) return;
|
if (_module == null) return;
|
||||||
|
|
||||||
@@ -62,13 +62,13 @@
|
|||||||
await InvokeAsync(StateHasChanged);
|
await InvokeAsync(StateHasChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void HandleActiveNodeChange(string nodeId)
|
private async Task HandleActiveNodeChange(string nodeId)
|
||||||
{
|
{
|
||||||
if (_module == null) return;
|
if (_module == null) return;
|
||||||
await _module.InvokeVoidAsync("setActiveNode", nodeId);
|
await _module.InvokeVoidAsync("setActiveNode", nodeId);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void HandleLoadingChange(bool isLoading)
|
private async Task HandleLoadingChange(bool isLoading)
|
||||||
{
|
{
|
||||||
await InvokeAsync(StateHasChanged);
|
await InvokeAsync(StateHasChanged);
|
||||||
}
|
}
|
||||||
@@ -81,7 +81,7 @@
|
|||||||
|
|
||||||
if (GraphService.CurrentGraphData != null)
|
if (GraphService.CurrentGraphData != null)
|
||||||
{
|
{
|
||||||
HandleGraphUpdate();
|
await HandleGraphUpdate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -100,7 +100,7 @@
|
|||||||
[JSInvokable]
|
[JSInvokable]
|
||||||
public async Task OnNodeClicked(string nodeId)
|
public async Task OnNodeClicked(string nodeId)
|
||||||
{
|
{
|
||||||
InteractionService.NotifyNodeSelected(nodeId);
|
await InteractionService.NotifyNodeSelected(nodeId);
|
||||||
|
|
||||||
if (OnNodeSelected.HasDelegate)
|
if (OnNodeSelected.HasDelegate)
|
||||||
{
|
{
|
||||||
@@ -109,7 +109,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private async void HandleFocusSimulation()
|
private async Task HandleFocusSimulation()
|
||||||
{
|
{
|
||||||
if (_module == null) return;
|
if (_module == null) return;
|
||||||
try
|
try
|
||||||
|
|||||||
@@ -98,3 +98,13 @@
|
|||||||
filter: drop-shadow(0 0 12px var(--nexus-neon));
|
filter: drop-shadow(0 0 12px var(--nexus-neon));
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
::deep @keyframes neon-flash {
|
||||||
|
0% { filter: brightness(1) drop-shadow(0 0 0px var(--nexus-neon)); }
|
||||||
|
50% { filter: brightness(3) drop-shadow(0 0 30px var(--nexus-neon)); }
|
||||||
|
100% { filter: brightness(1) drop-shadow(0 0 0px var(--nexus-neon)); }
|
||||||
|
}
|
||||||
|
|
||||||
|
::deep .neon-flash-node {
|
||||||
|
animation: neon-flash 0.8s ease-out;
|
||||||
|
}
|
||||||
|
|||||||
@@ -52,15 +52,16 @@
|
|||||||
private bool _isJsInitialized;
|
private bool _isJsInitialized;
|
||||||
private ElementReference _containerRef;
|
private ElementReference _containerRef;
|
||||||
|
|
||||||
protected override void OnInitialized()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
Coordinator.Clear();
|
await Coordinator.ClearAsync();
|
||||||
ThemeService.OnThemeChanged += StateHasChanged;
|
ThemeService.OnThemeChanged += HandleUpdate;
|
||||||
NavigationService.OnNavigationChanged += OnNavigationChanged;
|
NavigationService.OnNavigationChanged += OnNavigationChanged;
|
||||||
|
|
||||||
InteractionService.OnScrollToBlockRequested += HandleScrollRequested;
|
InteractionService.OnScrollToBlockRequested += HandleScrollRequested;
|
||||||
InteractionService.OnHighlightBlockRequested += HandleHighlightRequested;
|
InteractionService.OnHighlightBlockRequested += HandleHighlightRequested;
|
||||||
InteractionService.OnTextSelected += HandleTextSelected;
|
InteractionService.OnTextSelected += HandleTextSelected;
|
||||||
|
SyncService.OnProgressReceived += HandleSyncProgressReceived;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async Task OnParametersSetAsync()
|
protected override async Task OnParametersSetAsync()
|
||||||
@@ -113,60 +114,56 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
[JSInvokable]
|
[JSInvokable]
|
||||||
public void HandleBlockReached(string blockId, string content)
|
public async Task HandleBlockReached(string blockId, string content)
|
||||||
{
|
{
|
||||||
Coordinator.OnBlockReached(blockId, content);
|
await Coordinator.OnBlockReachedAsync(blockId, content);
|
||||||
|
|
||||||
// Debounce sync update (simple version: every 5 seconds or on a timer)
|
// Debounce sync update (simple version: every 5 seconds or on a timer)
|
||||||
_ = SyncService.UpdateProgressAsync(blockId);
|
await SyncService.UpdateProgressAsync(blockId);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void HandleSyncProgressReceived(string blockId, DateTime timestamp)
|
private async Task HandleSyncProgressReceived(string blockId, DateTime timestamp)
|
||||||
{
|
{
|
||||||
// For now, let's just scroll to the node if it's in the current view,
|
// For now, let's just scroll to the node if it's in the current view,
|
||||||
// or just log it. Usually, we should prompt the user.
|
// or just log it. Usually, we should prompt the user.
|
||||||
Console.WriteLine($"[Sync] Received progress from another device: {blockId} at {timestamp}");
|
Console.WriteLine($"[Sync] Received progress from another device: {blockId} at {timestamp}");
|
||||||
|
|
||||||
// Simple auto-scroll if it's newer than what we have (we don't track our own timestamp yet,
|
await ScrollToNodeAsync(blockId);
|
||||||
// but we can assume incoming syncs are from other active devices)
|
await InvokeAsync(StateHasChanged);
|
||||||
_ = InvokeAsync(async () => {
|
|
||||||
await ScrollToNodeAsync(blockId);
|
|
||||||
StateHasChanged();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[JSInvokable]
|
[JSInvokable]
|
||||||
public void HandleTextSelected(string text, string blockId, SelectionCoordinates coords)
|
public async Task HandleTextSelected(string text, string blockId, SelectionCoordinates coords)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"[ReaderCanvas] Text selected: {text} at {coords.Top},{coords.Left}");
|
Console.WriteLine($"[ReaderCanvas] Text selected: {text} at {coords.Top},{coords.Left}");
|
||||||
_selectedText = text;
|
_selectedText = text;
|
||||||
_selectedBlockId = blockId;
|
_selectedBlockId = blockId;
|
||||||
_selectionCoords = coords;
|
_selectionCoords = coords;
|
||||||
StateHasChanged();
|
await InvokeAsync(StateHasChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
[JSInvokable]
|
[JSInvokable]
|
||||||
public void HandleSelectionCleared()
|
public async Task HandleSelectionCleared()
|
||||||
{
|
{
|
||||||
_selectedText = string.Empty;
|
_selectedText = string.Empty;
|
||||||
_selectionCoords = null;
|
_selectionCoords = null;
|
||||||
StateHasChanged();
|
await InvokeAsync(StateHasChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void HandleScrollRequested(string blockId)
|
private async Task HandleScrollRequested(string blockId)
|
||||||
{
|
{
|
||||||
_ = ScrollToNodeAsync(blockId);
|
await ScrollToNodeAsync(blockId);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void HandleHighlightRequested(string blockId)
|
private async Task HandleHighlightRequested(string blockId)
|
||||||
{
|
{
|
||||||
_highlightedBlockId = blockId;
|
_highlightedBlockId = blockId;
|
||||||
StateHasChanged();
|
await InvokeAsync(StateHasChanged);
|
||||||
await Task.Delay(3000); // Highlight for 3 seconds
|
await Task.Delay(3000); // Highlight for 3 seconds
|
||||||
if (_highlightedBlockId == blockId)
|
if (_highlightedBlockId == blockId)
|
||||||
{
|
{
|
||||||
_highlightedBlockId = null;
|
_highlightedBlockId = null;
|
||||||
StateHasChanged();
|
await InvokeAsync(StateHasChanged);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,9 +209,11 @@
|
|||||||
catch { }
|
catch { }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Task HandleUpdate() => InvokeAsync(StateHasChanged);
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
ThemeService.OnThemeChanged -= StateHasChanged;
|
ThemeService.OnThemeChanged -= HandleUpdate;
|
||||||
NavigationService.OnNavigationChanged -= OnNavigationChanged;
|
NavigationService.OnNavigationChanged -= OnNavigationChanged;
|
||||||
|
|
||||||
InteractionService.OnScrollToBlockRequested -= HandleScrollRequested;
|
InteractionService.OnScrollToBlockRequested -= HandleScrollRequested;
|
||||||
|
|||||||
@@ -1,16 +1,47 @@
|
|||||||
.reader-canvas {
|
.reader-canvas {
|
||||||
max-width: 800px;
|
width: 100%;
|
||||||
margin: 0 auto;
|
height: 100%;
|
||||||
padding: 2rem 1rem;
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
padding: 2rem 0;
|
||||||
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
|
/* Dedicated Scrollbar Styling */
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: rgba(0, 255, 153, 0.2) transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reader-canvas::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reader-canvas::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reader-canvas::-webkit-scrollbar-thumb {
|
||||||
|
background-color: rgba(0, 255, 153, 0.2);
|
||||||
|
border-radius: 20px;
|
||||||
|
border: 3px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reader-canvas:hover::-webkit-scrollbar-thumb {
|
||||||
|
background-color: rgba(0, 255, 153, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.reader-canvas.theme-light {
|
||||||
|
background-color: #F9F9F9; /* Paper-white requirement */
|
||||||
}
|
}
|
||||||
|
|
||||||
.reader-flow-container {
|
.reader-flow-container {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 1.5rem;
|
gap: 1.5rem;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
padding: 0 1.5rem 15rem 1.5rem; /* Large padding-bottom for reachability */
|
||||||
}
|
}
|
||||||
|
|
||||||
.block-wrapper {
|
.block-wrapper {
|
||||||
@@ -20,6 +51,68 @@
|
|||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Typographic refinement for TextSegmentBlock */
|
||||||
|
::deep .nexus-ebook {
|
||||||
|
font-family: 'Merriweather', serif !important;
|
||||||
|
line-height: 1.65 !important;
|
||||||
|
letter-spacing: -0.01em !important;
|
||||||
|
font-size: 1.15rem;
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-light ::deep .nexus-ebook {
|
||||||
|
color: #1a1a1a;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Technical Code Block Container */
|
||||||
|
::deep .nexus-ebook pre {
|
||||||
|
background-color: #2d2d2d; /* Dark theme for code for better contrast */
|
||||||
|
color: #e0e0e0;
|
||||||
|
padding: 1.25rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin: 2rem 0;
|
||||||
|
overflow-x: auto;
|
||||||
|
box-shadow: inset 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
border-left: 4px solid var(--nexus-neon); /* Nexus neon accent */
|
||||||
|
|
||||||
|
/* Dedicated Scrollbar for Code */
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: rgba(0, 255, 153, 0.3) transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
::deep .nexus-ebook pre::-webkit-scrollbar {
|
||||||
|
height: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::deep .nexus-ebook pre::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba(0, 255, 153, 0.3);
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Monospace Typography Contrast */
|
||||||
|
::deep .nexus-ebook code {
|
||||||
|
font-family: 'JetBrains Mono', 'Cascadia Code', 'Consolas', monospace !important;
|
||||||
|
font-variant-ligatures: contextual;
|
||||||
|
line-height: 1.5;
|
||||||
|
tab-size: 4;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Inline Code Highlight */
|
||||||
|
::deep .nexus-ebook p code {
|
||||||
|
background-color: rgba(0, 0, 0, 0.05);
|
||||||
|
color: #d63384; /* Classic differentiator for inline code */
|
||||||
|
padding: 0.2rem 0.4rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-dark ::deep .nexus-ebook p code {
|
||||||
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
|
color: #ff79c6;
|
||||||
|
}
|
||||||
|
|
||||||
.block-wrapper.highlighted {
|
.block-wrapper.highlighted {
|
||||||
background: rgba(0, 243, 255, 0.08);
|
background: rgba(0, 243, 255, 0.08);
|
||||||
box-shadow: 0 0 20px rgba(0, 243, 255, 0.15);
|
box-shadow: 0 0 20px rgba(0, 243, 255, 0.15);
|
||||||
|
|||||||
@@ -77,8 +77,8 @@
|
|||||||
|
|
||||||
protected override void OnInitialized()
|
protected override void OnInitialized()
|
||||||
{
|
{
|
||||||
FocusMode.OnFocusModeChanged += StateHasChanged;
|
FocusMode.OnFocusModeChanged += HandleUpdate;
|
||||||
QuizService.OnQuizUpdated += StateHasChanged;
|
QuizService.OnQuizUpdated += HandleUpdate;
|
||||||
|
|
||||||
var context = PlatformService.GetDeviceContext();
|
var context = PlatformService.GetDeviceContext();
|
||||||
if (context.IsSuccess)
|
if (context.IsSuccess)
|
||||||
@@ -115,9 +115,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Task HandleUpdate() => InvokeAsync(StateHasChanged);
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
FocusMode.OnFocusModeChanged -= StateHasChanged;
|
FocusMode.OnFocusModeChanged -= HandleUpdate;
|
||||||
QuizService.OnQuizUpdated -= StateHasChanged;
|
QuizService.OnQuizUpdated -= HandleUpdate;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,9 +20,10 @@
|
|||||||
|
|
||||||
main {
|
main {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: auto;
|
overflow: hidden;
|
||||||
overflow-x: hidden;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.intelligence-sidebar {
|
.intelligence-sidebar {
|
||||||
|
|||||||
@@ -22,8 +22,8 @@
|
|||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
QuizState.OnQuizRequested += HandleQuizRequested;
|
QuizState.OnQuizRequested += HandleQuizRequestedAsync;
|
||||||
FocusMode.OnFocusModeChanged += StateHasChanged;
|
FocusMode.OnFocusModeChanged += HandleUpdate;
|
||||||
await FocusMode.InitializeAsync();
|
await FocusMode.InitializeAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,16 +54,18 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void HandleQuizRequested(string blockId)
|
private async Task HandleQuizRequestedAsync(string blockId)
|
||||||
{
|
{
|
||||||
_activeQuizBlockId = blockId;
|
_activeQuizBlockId = blockId;
|
||||||
StateHasChanged();
|
await InvokeAsync(StateHasChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Task HandleUpdate() => InvokeAsync(StateHasChanged);
|
||||||
|
|
||||||
public async ValueTask DisposeAsync()
|
public async ValueTask DisposeAsync()
|
||||||
{
|
{
|
||||||
QuizState.OnQuizRequested -= HandleQuizRequested;
|
QuizState.OnQuizRequested -= HandleQuizRequestedAsync;
|
||||||
FocusMode.OnFocusModeChanged -= StateHasChanged;
|
FocusMode.OnFocusModeChanged -= HandleUpdate;
|
||||||
|
|
||||||
if (_interopModule != null && _keydownHandler != null)
|
if (_interopModule != null && _keydownHandler != null)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
.home-reader-container {
|
.home-reader-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
max-width: 800px;
|
max-width: 800px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 2rem;
|
|
||||||
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
@page "/not-found"
|
@page "/not-found"
|
||||||
@layout MainLayout
|
@layout Layout.MainLayout
|
||||||
|
|
||||||
<h3>Not Found</h3>
|
<div class="not-found-preloader">
|
||||||
<p>Sorry, the content you are looking for does not exist.</p>
|
<div class="preloader-robot">
|
||||||
|
<NexusIcon Name="robot" Size="64" class="neon-pulse" />
|
||||||
|
<div class="scan-line"></div>
|
||||||
|
</div>
|
||||||
|
<NexusTypography Variant="NexusTypography.TypographyVariant.UI">Synchronizowanie przestrzeni Nexus...</NexusTypography>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
.not-found-preloader {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 60vh;
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preloader-robot {
|
||||||
|
position: relative;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scan-line {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 2px;
|
||||||
|
background: var(--nexus-neon);
|
||||||
|
box-shadow: 0 0 10px var(--nexus-neon);
|
||||||
|
animation: scan 2s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes scan {
|
||||||
|
0% { top: 0; }
|
||||||
|
50% { top: 100%; }
|
||||||
|
100% { top: 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.neon-pulse {
|
||||||
|
color: var(--nexus-neon);
|
||||||
|
filter: drop-shadow(0 0 5px var(--nexus-neon));
|
||||||
|
animation: pulse 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0% { transform: scale(1); opacity: 1; }
|
||||||
|
50% { transform: scale(1.1); opacity: 0.8; }
|
||||||
|
100% { transform: scale(1); opacity: 1; }
|
||||||
|
}
|
||||||
@@ -9,6 +9,9 @@
|
|||||||
</AuthorizeRouteView>
|
</AuthorizeRouteView>
|
||||||
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
|
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
|
||||||
</Found>
|
</Found>
|
||||||
|
<NotFound>
|
||||||
|
<NexusReader.UI.Shared.Pages.NotFound />
|
||||||
|
</NotFound>
|
||||||
</Router>
|
</Router>
|
||||||
</ChildContent>
|
</ChildContent>
|
||||||
<ErrorContent Context="ex">
|
<ErrorContent Context="ex">
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ public sealed class FocusModeService : IFocusModeService
|
|||||||
{
|
{
|
||||||
private readonly IJSRuntime _jsRuntime;
|
private readonly IJSRuntime _jsRuntime;
|
||||||
public bool IsFocusModeActive { get; private set; }
|
public bool IsFocusModeActive { get; private set; }
|
||||||
public event Action? OnFocusModeChanged;
|
public event Func<Task>? OnFocusModeChanged;
|
||||||
|
|
||||||
public FocusModeService(IJSRuntime jsRuntime)
|
public FocusModeService(IJSRuntime jsRuntime)
|
||||||
{
|
{
|
||||||
@@ -21,7 +21,7 @@ public sealed class FocusModeService : IFocusModeService
|
|||||||
if (value == "true" && !IsFocusModeActive)
|
if (value == "true" && !IsFocusModeActive)
|
||||||
{
|
{
|
||||||
IsFocusModeActive = true;
|
IsFocusModeActive = true;
|
||||||
OnFocusModeChanged?.Invoke();
|
if (OnFocusModeChanged != null) await OnFocusModeChanged();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
@@ -33,7 +33,7 @@ public sealed class FocusModeService : IFocusModeService
|
|||||||
public async Task ToggleAsync()
|
public async Task ToggleAsync()
|
||||||
{
|
{
|
||||||
IsFocusModeActive = !IsFocusModeActive;
|
IsFocusModeActive = !IsFocusModeActive;
|
||||||
OnFocusModeChanged?.Invoke();
|
if (OnFocusModeChanged != null) await OnFocusModeChanged();
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ namespace NexusReader.UI.Shared.Services;
|
|||||||
public interface IFocusModeService
|
public interface IFocusModeService
|
||||||
{
|
{
|
||||||
bool IsFocusModeActive { get; }
|
bool IsFocusModeActive { get; }
|
||||||
event Action? OnFocusModeChanged;
|
event Func<Task>? OnFocusModeChanged;
|
||||||
Task InitializeAsync();
|
Task InitializeAsync();
|
||||||
Task ToggleAsync();
|
Task ToggleAsync();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,12 +8,12 @@ public interface IKnowledgeGraphService
|
|||||||
string? ActiveNodeId { get; }
|
string? ActiveNodeId { get; }
|
||||||
bool IsLoading { get; }
|
bool IsLoading { get; }
|
||||||
|
|
||||||
event Action? OnGraphUpdated;
|
event Func<Task>? OnGraphUpdated;
|
||||||
event Action<string>? OnActiveNodeChanged;
|
event Func<string, Task>? OnActiveNodeChanged;
|
||||||
event Action<bool>? OnLoadingChanged;
|
event Func<bool, Task>? OnLoadingChanged;
|
||||||
|
|
||||||
void UpdateGraph(GraphDataDto newData);
|
Task UpdateGraph(GraphDataDto newData);
|
||||||
void SetActiveNode(string nodeId);
|
Task SetActiveNode(string nodeId);
|
||||||
void SetLoading(bool isLoading);
|
Task SetLoading(bool isLoading);
|
||||||
void Clear();
|
Task Clear();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,11 +9,11 @@ public interface IQuizStateService
|
|||||||
bool IsHydrating { get; }
|
bool IsHydrating { get; }
|
||||||
bool HasNewQuiz { get; }
|
bool HasNewQuiz { get; }
|
||||||
|
|
||||||
event Action<string>? OnQuizRequested;
|
event Func<string, Task>? OnQuizRequested;
|
||||||
event Action? OnQuizUpdated;
|
event Func<Task>? OnQuizUpdated;
|
||||||
|
|
||||||
void RequestQuiz(string blockId);
|
Task RequestQuiz(string blockId);
|
||||||
void SetQuiz(string? blockId, QuizDto quiz);
|
Task SetQuiz(string? blockId, QuizDto? quiz);
|
||||||
void SetHydrating(bool hydrating);
|
Task SetHydrating(bool hydrating);
|
||||||
void MarkQuizAsSeen();
|
Task MarkQuizAsSeen();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,15 +2,15 @@ namespace NexusReader.UI.Shared.Services;
|
|||||||
|
|
||||||
public interface IReaderInteractionService
|
public interface IReaderInteractionService
|
||||||
{
|
{
|
||||||
event Action<string>? OnNodeSelected;
|
event Func<string, Task>? OnNodeSelected;
|
||||||
event Action<string>? OnScrollToBlockRequested;
|
event Func<string, Task>? OnScrollToBlockRequested;
|
||||||
event Action<string>? OnHighlightBlockRequested;
|
event Func<string, Task>? OnHighlightBlockRequested;
|
||||||
event Action<string, string, SelectionCoordinates>? OnTextSelected;
|
event Func<string, string, SelectionCoordinates, Task>? OnTextSelected;
|
||||||
|
|
||||||
void NotifyNodeSelected(string nodeId);
|
Task NotifyNodeSelected(string nodeId);
|
||||||
void RequestScrollToBlock(string blockId);
|
Task RequestScrollToBlock(string blockId);
|
||||||
void RequestHighlightBlock(string blockId);
|
Task RequestHighlightBlock(string blockId);
|
||||||
void NotifyTextSelected(string text, string blockId, SelectionCoordinates coords);
|
Task NotifyTextSelected(string text, string blockId, SelectionCoordinates coords);
|
||||||
}
|
}
|
||||||
|
|
||||||
public record SelectionCoordinates(double Top, double Left, double Width);
|
public record SelectionCoordinates(double Top, double Left, double Width);
|
||||||
|
|||||||
@@ -6,6 +6,6 @@ public interface ISyncService
|
|||||||
{
|
{
|
||||||
Task<Result> InitializeAsync();
|
Task<Result> InitializeAsync();
|
||||||
Task<Result> UpdateProgressAsync(string pageId);
|
Task<Result> UpdateProgressAsync(string pageId);
|
||||||
event Action<string, DateTime> OnProgressReceived;
|
event Func<string, DateTime, Task> OnProgressReceived;
|
||||||
Task DisposeAsync();
|
Task DisposeAsync();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,6 @@ namespace NexusReader.UI.Shared.Services;
|
|||||||
public interface IThemeService
|
public interface IThemeService
|
||||||
{
|
{
|
||||||
bool IsLightMode { get; }
|
bool IsLightMode { get; }
|
||||||
event Action? OnThemeChanged;
|
event Func<Task>? OnThemeChanged;
|
||||||
void ToggleTheme();
|
Task ToggleTheme();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,10 +36,10 @@ public sealed partial class KnowledgeCoordinator : IDisposable
|
|||||||
_interactionService.OnNodeSelected += HandleNodeSelected;
|
_interactionService.OnNodeSelected += HandleNodeSelected;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void HandleNodeSelected(string nodeId)
|
private async Task HandleNodeSelected(string nodeId)
|
||||||
{
|
{
|
||||||
_interactionService.RequestScrollToBlock(nodeId);
|
await _interactionService.RequestScrollToBlock(nodeId);
|
||||||
_interactionService.RequestHighlightBlock(nodeId);
|
await _interactionService.RequestHighlightBlock(nodeId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task ProcessFullPageAsync(string fullContent, string tenantId = "global")
|
public async Task ProcessFullPageAsync(string fullContent, string tenantId = "global")
|
||||||
@@ -48,8 +48,8 @@ public sealed partial class KnowledgeCoordinator : IDisposable
|
|||||||
|
|
||||||
LogGeneratingGraph(tenantId);
|
LogGeneratingGraph(tenantId);
|
||||||
|
|
||||||
_graphService.Clear();
|
await _graphService.Clear();
|
||||||
_graphService.SetLoading(true);
|
await _graphService.SetLoading(true);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -59,7 +59,7 @@ public sealed partial class KnowledgeCoordinator : IDisposable
|
|||||||
var packet = result.Value;
|
var packet = result.Value;
|
||||||
if (packet.Graph != null)
|
if (packet.Graph != null)
|
||||||
{
|
{
|
||||||
_graphService.UpdateGraph(packet.Graph);
|
await _graphService.UpdateGraph(packet.Graph);
|
||||||
OnGraphUpdated?.Invoke(packet.Graph);
|
OnGraphUpdated?.Invoke(packet.Graph);
|
||||||
await _platformService.VibrateSuccessAsync();
|
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
|
// 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")
|
public async Task<KnowledgePacket?> RequestSummaryAndQuizAsync(string content, string tenantId = "global")
|
||||||
@@ -109,9 +109,9 @@ public sealed partial class KnowledgeCoordinator : IDisposable
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Clear()
|
public async Task ClearAsync()
|
||||||
{
|
{
|
||||||
_graphService.Clear();
|
await _graphService.Clear();
|
||||||
_quizService.SetQuiz(null, null);
|
_quizService.SetQuiz(null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,36 +8,36 @@ public sealed class KnowledgeGraphService : IKnowledgeGraphService
|
|||||||
public string? ActiveNodeId { get; private set; }
|
public string? ActiveNodeId { get; private set; }
|
||||||
public bool IsLoading { get; private set; }
|
public bool IsLoading { get; private set; }
|
||||||
|
|
||||||
public event Action? OnGraphUpdated;
|
public event Func<Task>? OnGraphUpdated;
|
||||||
public event Action<string>? OnActiveNodeChanged;
|
public event Func<string, Task>? OnActiveNodeChanged;
|
||||||
public event Action<bool>? OnLoadingChanged;
|
public event Func<bool, Task>? OnLoadingChanged;
|
||||||
|
|
||||||
public void UpdateGraph(GraphDataDto newData)
|
public async Task UpdateGraph(GraphDataDto newData)
|
||||||
{
|
{
|
||||||
CurrentGraphData = newData;
|
CurrentGraphData = newData;
|
||||||
IsLoading = false;
|
IsLoading = false;
|
||||||
OnLoadingChanged?.Invoke(false);
|
if (OnLoadingChanged != null) await OnLoadingChanged(false);
|
||||||
OnGraphUpdated?.Invoke();
|
if (OnGraphUpdated != null) await OnGraphUpdated();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetActiveNode(string nodeId)
|
public async Task SetActiveNode(string nodeId)
|
||||||
{
|
{
|
||||||
if (ActiveNodeId == nodeId) return;
|
if (ActiveNodeId == nodeId) return;
|
||||||
ActiveNodeId = nodeId;
|
ActiveNodeId = nodeId;
|
||||||
OnActiveNodeChanged?.Invoke(nodeId);
|
if (OnActiveNodeChanged != null) await OnActiveNodeChanged(nodeId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetLoading(bool isLoading)
|
public async Task SetLoading(bool isLoading)
|
||||||
{
|
{
|
||||||
IsLoading = isLoading;
|
IsLoading = isLoading;
|
||||||
OnLoadingChanged?.Invoke(isLoading);
|
if (OnLoadingChanged != null) await OnLoadingChanged(isLoading);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Clear()
|
public async Task Clear()
|
||||||
{
|
{
|
||||||
CurrentGraphData = null;
|
CurrentGraphData = null;
|
||||||
ActiveNodeId = null;
|
ActiveNodeId = null;
|
||||||
IsLoading = false;
|
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 IsHydrating { get; private set; }
|
||||||
public bool HasNewQuiz { get; private set; }
|
public bool HasNewQuiz { get; private set; }
|
||||||
|
|
||||||
public event Action<string>? OnQuizRequested;
|
public event Func<string, Task>? OnQuizRequested;
|
||||||
public event Action? OnQuizUpdated;
|
public event Func<Task>? OnQuizUpdated;
|
||||||
|
|
||||||
public void RequestQuiz(string blockId)
|
public async Task RequestQuiz(string blockId)
|
||||||
{
|
{
|
||||||
CurrentQuizBlockId = 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;
|
CurrentQuizBlockId = blockId;
|
||||||
CurrentQuiz = quiz;
|
CurrentQuiz = quiz;
|
||||||
IsHydrating = false;
|
IsHydrating = false;
|
||||||
HasNewQuiz = true;
|
HasNewQuiz = quiz != null;
|
||||||
OnQuizUpdated?.Invoke();
|
if (OnQuizUpdated != null) await OnQuizUpdated();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetHydrating(bool hydrating)
|
public async Task SetHydrating(bool hydrating)
|
||||||
{
|
{
|
||||||
IsHydrating = hydrating;
|
IsHydrating = hydrating;
|
||||||
OnQuizUpdated?.Invoke();
|
if (OnQuizUpdated != null) await OnQuizUpdated();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void MarkQuizAsSeen()
|
public async Task MarkQuizAsSeen()
|
||||||
{
|
{
|
||||||
if (!HasNewQuiz) return;
|
if (!HasNewQuiz) return;
|
||||||
HasNewQuiz = false;
|
HasNewQuiz = false;
|
||||||
OnQuizUpdated?.Invoke();
|
if (OnQuizUpdated != null) await OnQuizUpdated();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,28 +2,28 @@ namespace NexusReader.UI.Shared.Services;
|
|||||||
|
|
||||||
public sealed class ReaderInteractionService : IReaderInteractionService
|
public sealed class ReaderInteractionService : IReaderInteractionService
|
||||||
{
|
{
|
||||||
public event Action<string>? OnNodeSelected;
|
public event Func<string, Task>? OnNodeSelected;
|
||||||
public event Action<string>? OnScrollToBlockRequested;
|
public event Func<string, Task>? OnScrollToBlockRequested;
|
||||||
public event Action<string>? OnHighlightBlockRequested;
|
public event Func<string, Task>? OnHighlightBlockRequested;
|
||||||
public event Action<string, string, SelectionCoordinates>? OnTextSelected;
|
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 bool _isInitialized;
|
||||||
private CancellationTokenSource? _debounceCts;
|
private CancellationTokenSource? _debounceCts;
|
||||||
|
|
||||||
public event Action<string, DateTime>? OnProgressReceived;
|
public event Func<string, DateTime, Task>? OnProgressReceived;
|
||||||
|
|
||||||
public SyncService(
|
public SyncService(
|
||||||
HttpClient httpClient,
|
HttpClient httpClient,
|
||||||
@@ -44,9 +44,9 @@ public class SyncService : ISyncService, IAsyncDisposable
|
|||||||
.WithAutomaticReconnect()
|
.WithAutomaticReconnect()
|
||||||
.Build();
|
.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
|
try
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ namespace NexusReader.UI.Shared.Services;
|
|||||||
public sealed class ThemeService : IThemeService
|
public sealed class ThemeService : IThemeService
|
||||||
{
|
{
|
||||||
public bool IsLightMode { get; private set; } = false;
|
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;
|
IsLightMode = !IsLightMode;
|
||||||
OnThemeChanged?.Invoke();
|
if (OnThemeChanged != null) await OnThemeChanged();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -134,9 +134,10 @@ export function updateData(data) {
|
|||||||
.attr("fill", "none")
|
.attr("fill", "none")
|
||||||
.attr("stroke-width", d => d.relationType === 'Defines' ? 2 : 1)
|
.attr("stroke-width", d => d.relationType === 'Defines' ? 2 : 1)
|
||||||
.attr("stroke-dasharray", d => d.relationType === 'References' ? "5,5" : "0")
|
.attr("stroke-dasharray", d => d.relationType === 'References' ? "5,5" : "0")
|
||||||
.call(e => e.transition().duration(500).attr("opacity", 1)),
|
.style("opacity", 0)
|
||||||
|
.call(enter => enter.transition().duration(500).style("opacity", 1)),
|
||||||
update => update,
|
update => update,
|
||||||
exit => exit.remove()
|
exit => exit.transition().duration(500).style("opacity", 0).remove()
|
||||||
);
|
);
|
||||||
|
|
||||||
// Update Nodes
|
// Update Nodes
|
||||||
@@ -146,8 +147,9 @@ export function updateData(data) {
|
|||||||
.join(
|
.join(
|
||||||
enter => {
|
enter => {
|
||||||
const g = enter.append("g")
|
const g = enter.append("g")
|
||||||
.attr("class", "node-group")
|
.attr("class", "node-group neon-flash-node")
|
||||||
.style("cursor", "pointer")
|
.style("cursor", "pointer")
|
||||||
|
.style("opacity", 0)
|
||||||
.on("click", (e, d) => {
|
.on("click", (e, d) => {
|
||||||
currentDotNetHelper.invokeMethodAsync('OnNodeClicked', d.id);
|
currentDotNetHelper.invokeMethodAsync('OnNodeClicked', d.id);
|
||||||
setActiveNode(d.id);
|
setActiveNode(d.id);
|
||||||
@@ -162,8 +164,7 @@ export function updateData(data) {
|
|||||||
if (d.type === 'Rule') return '#ff4444';
|
if (d.type === 'Rule') return '#ff4444';
|
||||||
return "url(#nebulaGlow)";
|
return "url(#nebulaGlow)";
|
||||||
})
|
})
|
||||||
.attr("opacity", 0)
|
.attr("opacity", d => d.group === 'current' ? 0.6 : 0.2);
|
||||||
.transition().duration(1000).attr("opacity", d => d.group === 'current' ? 0.6 : 0.2);
|
|
||||||
|
|
||||||
g.append("rect")
|
g.append("rect")
|
||||||
.attr("class", "node-pill")
|
.attr("class", "node-pill")
|
||||||
@@ -187,10 +188,12 @@ export function updateData(data) {
|
|||||||
.attr("fill", d => d.type === 'Definition' ? 'var(--nexus-accent)' : '#ccc')
|
.attr("fill", d => d.type === 'Definition' ? 'var(--nexus-accent)' : '#ccc')
|
||||||
.attr("font-size", "0.8rem");
|
.attr("font-size", "0.8rem");
|
||||||
|
|
||||||
|
g.transition().duration(500).style("opacity", 1);
|
||||||
|
|
||||||
return g;
|
return g;
|
||||||
},
|
},
|
||||||
update => update,
|
update => update.classed("neon-flash-node", false),
|
||||||
exit => exit.remove()
|
exit => exit.transition().duration(500).style("opacity", 0).remove()
|
||||||
);
|
);
|
||||||
|
|
||||||
simulation.nodes(data.nodes);
|
simulation.nodes(data.nodes);
|
||||||
@@ -223,7 +226,11 @@ export function setActiveNode(nodeId) {
|
|||||||
if (!svgElement || !node) return;
|
if (!svgElement || !node) return;
|
||||||
|
|
||||||
const targetNode = node.filter(d => d.id === nodeId);
|
const targetNode = node.filter(d => d.id === nodeId);
|
||||||
if (targetNode.empty()) return;
|
if (targetNode.empty()) {
|
||||||
|
dimNodes(null);
|
||||||
|
badge.style("display", "none");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const d = targetNode.datum();
|
const d = targetNode.datum();
|
||||||
|
|
||||||
@@ -235,6 +242,9 @@ export function setActiveNode(nodeId) {
|
|||||||
badge.style("display", "block").datum(d);
|
badge.style("display", "block").datum(d);
|
||||||
badge.attr("transform", `translate(${d.x},${d.y})`);
|
badge.attr("transform", `translate(${d.x},${d.y})`);
|
||||||
|
|
||||||
|
// Dim others
|
||||||
|
dimNodes(nodeId);
|
||||||
|
|
||||||
// Smooth transition
|
// Smooth transition
|
||||||
svgElement.transition().duration(1000).call(
|
svgElement.transition().duration(1000).call(
|
||||||
zoomBehavior.transform,
|
zoomBehavior.transform,
|
||||||
@@ -242,6 +252,24 @@ export function setActiveNode(nodeId) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function dimNodes(activeNodeId) {
|
||||||
|
if (!node) return;
|
||||||
|
|
||||||
|
node.transition().duration(500)
|
||||||
|
.style("opacity", d => (activeNodeId === null || d.id === activeNodeId) ? 1 : 0.4);
|
||||||
|
|
||||||
|
if (link) {
|
||||||
|
link.transition().duration(500)
|
||||||
|
.style("opacity", d => {
|
||||||
|
if (activeNodeId === null) return 1;
|
||||||
|
// Check if this link is connected to the active node
|
||||||
|
const sourceId = typeof d.source === 'object' ? d.source.id : d.source;
|
||||||
|
const targetId = typeof d.target === 'object' ? d.target.id : d.target;
|
||||||
|
return (sourceId === activeNodeId || targetId === activeNodeId) ? 1 : 0.1;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function unmount(containerId) {
|
export function unmount(containerId) {
|
||||||
if (simulation) {
|
if (simulation) {
|
||||||
simulation.stop();
|
simulation.stop();
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ using NexusReader.Application.Abstractions.Services;
|
|||||||
using NexusReader.Web.Client.Services;
|
using NexusReader.Web.Client.Services;
|
||||||
using NexusReader.UI.Shared.Services;
|
using NexusReader.UI.Shared.Services;
|
||||||
using NexusReader.Application;
|
using NexusReader.Application;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.AI;
|
||||||
|
using NexusReader.Data.Persistence;
|
||||||
|
|
||||||
|
|
||||||
var builder = WebAssemblyHostBuilder.CreateDefault(args);
|
var builder = WebAssemblyHostBuilder.CreateDefault(args);
|
||||||
@@ -32,7 +35,24 @@ builder.Services.AddCascadingAuthenticationState();
|
|||||||
builder.Services.AddScoped<IKnowledgeService, WasmKnowledgeService>();
|
builder.Services.AddScoped<IKnowledgeService, WasmKnowledgeService>();
|
||||||
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
|
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
|
||||||
|
|
||||||
|
// Dummy registrations for server-only handlers to satisfy DI validation
|
||||||
|
builder.Services.AddSingleton<IDbContextFactory<AppDbContext>>(new ThrowingDbContextFactory());
|
||||||
|
builder.Services.AddSingleton<IEmbeddingGenerator<string, Embedding<float>>>(new ThrowingEmbeddingGenerator());
|
||||||
|
|
||||||
builder.Services.AddApplication();
|
builder.Services.AddApplication();
|
||||||
builder.Services.AddScoped<IEpubService, WasmEpubService>();
|
builder.Services.AddScoped<IEpubService, WasmEpubService>();
|
||||||
|
|
||||||
await builder.Build().RunAsync();
|
await builder.Build().RunAsync();
|
||||||
|
|
||||||
|
public class ThrowingDbContextFactory : IDbContextFactory<AppDbContext>
|
||||||
|
{
|
||||||
|
public AppDbContext CreateDbContext() => throw new NotSupportedException("DbContext cannot be used in WASM client.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ThrowingEmbeddingGenerator : IEmbeddingGenerator<string, Embedding<float>>
|
||||||
|
{
|
||||||
|
public void Dispose() { }
|
||||||
|
public Task<GeneratedEmbeddings<Embedding<float>>> GenerateAsync(IEnumerable<string> values, EmbeddingGenerationOptions? options = null, CancellationToken cancellationToken = default)
|
||||||
|
=> throw new NotSupportedException("Embedding generation cannot be used in WASM client.");
|
||||||
|
public object? GetService(Type serviceType, object? serviceKey = null) => null;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user