From 55cc3ae10d4c68d0920e72e3688cb026ab5aab05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Jasi=C5=84ski?= Date: Fri, 8 May 2026 18:16:09 +0000 Subject: [PATCH] feat(ui/arch): Optimize Graph Dynamics, Immersive Reader, and Core Stability (#19) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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`. - 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: https://git.archimap.cloud/mjasin/Nexus.Reader/pulls/19 Co-authored-by: Marek JasiƄski Co-committed-by: Marek JasiƄski --- .../nexus-architecture-standards/SKILL.md | 7 +- .agent/skills/nexus-graph-d3/SKILL.md | 3 + .agent/skills/nexus-ui-engine/SKILL.md | 7 +- .../DependencyInjection.cs | 1 + .../Services/EpubService.cs | 58 +++++++---- .../Components/Atoms/NexusIcon.razor | 6 ++ .../Molecules/AiAssistantBubble.razor | 8 +- .../Molecules/IntelligenceToolbar.razor | 6 +- .../Components/Molecules/KnowledgeCheck.razor | 6 +- .../Molecules/SelectionAiPanel.razor | 16 +-- .../Components/Organisms/KnowledgeGraph.razor | 12 +-- .../Organisms/KnowledgeGraph.razor.css | 10 ++ .../Components/Organisms/ReaderCanvas.razor | 45 +++++---- .../Organisms/ReaderCanvas.razor.css | 99 ++++++++++++++++++- .../Components/Organisms/ReaderFooter.razor | 5 +- .../Organisms/ReaderFooter.razor.css | 19 ++-- .../Layout/MainLayout.razor | 10 +- .../Layout/MainLayout.razor.css | 5 +- src/NexusReader.UI.Shared/Pages/Home.razor | 14 +-- .../Pages/Home.razor.css | 2 +- .../Pages/NotFound.razor | 15 ++- .../Pages/NotFound.razor.css | 42 ++++++++ src/NexusReader.UI.Shared/Routes.razor | 3 + .../Services/FocusModeService.cs | 6 +- .../Services/IFocusModeService.cs | 2 +- .../Services/IKnowledgeGraphService.cs | 14 +-- .../Services/IQuizStateService.cs | 12 +-- .../Services/IReaderInteractionService.cs | 16 +-- .../Services/ISyncService.cs | 2 +- .../Services/IThemeService.cs | 4 +- .../Services/KnowledgeCoordinator.cs | 20 ++-- .../Services/KnowledgeGraphService.cs | 24 ++--- .../Services/QuizStateService.cs | 22 ++--- .../Services/ReaderInteractionService.cs | 24 ++--- .../Services/SyncService.cs | 6 +- .../Services/ThemeService.cs | 6 +- .../wwwroot/js/knowledgeGraph.js | 44 +++++++-- src/NexusReader.Web.Client/Program.cs | 20 ++++ 38 files changed, 442 insertions(+), 179 deletions(-) create mode 100644 src/NexusReader.UI.Shared/Pages/NotFound.razor.css diff --git a/.agent/skills/nexus-architecture-standards/SKILL.md b/.agent/skills/nexus-architecture-standards/SKILL.md index 97bd670..1115a8e 100644 --- a/.agent/skills/nexus-architecture-standards/SKILL.md +++ b/.agent/skills/nexus-architecture-standards/SKILL.md @@ -29,9 +29,12 @@ This skill defines the architectural guardrails for the NexusReader project to e - Use `FluentResults` (`Result`) for all Application services and handlers. - Avoid throwing exceptions for expected business failures; use `Result.Fail()`. -### 4. MediatR Patterns -- **Queries**: Read-only operations. Should return `Result`. Use `AsNoTracking()` in EF Core. - **Commands**: State-changing operations. Should return `Result` or `Result`. ++ ++### 5. Async Operations (Zero Tolerance for `async void`) ++- All asynchronous operations MUST return `Task` or `ValueTask`. ++- Event handlers MUST use `Func` or async-compatible patterns. ++- UI components MUST await all service calls and use `InvokeAsync(StateHasChanged)` for state updates within async contexts. ## Audit Scripts - [ArchCheck.sh](scripts/arch_check.sh): A shell script to scan for illegal cross-layer imports. diff --git a/.agent/skills/nexus-graph-d3/SKILL.md b/.agent/skills/nexus-graph-d3/SKILL.md index bc008a1..f8d9efd 100644 --- a/.agent/skills/nexus-graph-d3/SKILL.md +++ b/.agent/skills/nexus-graph-d3/SKILL.md @@ -8,4 +8,7 @@ description: D3.js standards for Knowledge Graph - **JS Interop:** Use ES6 modules and `IJSObjectReference`. - **Responsiveness:** SVG must use `viewBox` for fluid portrait scaling. - **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`. \ No newline at end of file diff --git a/.agent/skills/nexus-ui-engine/SKILL.md b/.agent/skills/nexus-ui-engine/SKILL.md index 1cd795a..120a706 100644 --- a/.agent/skills/nexus-ui-engine/SKILL.md +++ b/.agent/skills/nexus-ui-engine/SKILL.md @@ -22,7 +22,7 @@ description: Design System & Component rules for Blazor - Light Mode: `--nexus-bg` (`#f8f9fa`), `--nexus-card` (`#ffffff`). - **Typography:** - 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:** - Subtle neon glows (`box-shadow: 0 0 15px rgba(0, 255, 153, 0.3)`). - Glassmorphism for overlays and modals. @@ -30,6 +30,11 @@ description: Design System & Component rules for Blazor - **Adaptive Layouts:** - Support `.platform-mobile` and `.platform-desktop` context classes. - 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):** - Touch Targets: Min `44x44px` on mobile (enforced via CSS variables). diff --git a/src/NexusReader.Application/DependencyInjection.cs b/src/NexusReader.Application/DependencyInjection.cs index 071c8e4..97acbbe 100644 --- a/src/NexusReader.Application/DependencyInjection.cs +++ b/src/NexusReader.Application/DependencyInjection.cs @@ -8,6 +8,7 @@ public static class DependencyInjection public static IServiceCollection AddApplication(this IServiceCollection services) { services.AddMapsterConfiguration(); + services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(DependencyInjection).Assembly)); return services; } diff --git a/src/NexusReader.Infrastructure/Services/EpubService.cs b/src/NexusReader.Infrastructure/Services/EpubService.cs index f05b8f1..e7a5dd0 100644 --- a/src/NexusReader.Infrastructure/Services/EpubService.cs +++ b/src/NexusReader.Infrastructure/Services/EpubService.cs @@ -46,34 +46,35 @@ public class EpubService : IEpubService return Result.Fail($"EPUB file at '{fullPath}' is not accessible or does not exist."); } - EpubBook book; - try - { - book = await EpubReader.ReadBookAsync(fullPath); - } - catch (Exception ex) - { - return Result.Fail(new Error($"Failed to parse EPUB file. It might be corrupted or in use. Path: {fullPath}").CausedBy(ex)); - } - var blocks = new List(); - int totalWordCount = 0; - int blockCounter = 0; + using var bookRef = await EpubReader.OpenBookAsync(fullPath); + var readingOrder = bookRef.GetReadingOrder(); - if (book.ReadingOrder == null || !book.ReadingOrder.Any()) + if (readingOrder == null || !readingOrder.Any()) { return Result.Fail("The EPUB has no readable content files in ReadingOrder."); } // Ensure index is within bounds - if (chapterIndex < 0 || chapterIndex >= book.ReadingOrder.Count) + if (chapterIndex < 0 || chapterIndex >= readingOrder.Count) { chapterIndex = 0; // Default to first chapter } - var chapter = book.ReadingOrder[chapterIndex]; - var chapterTitle = chapter.FilePath ?? $"Chapter {chapterIndex + 1}"; + var chapterRef = readingOrder[chapterIndex]; + + // Try to find a better title from navigation (TOC) + var navigation = bookRef.GetNavigation(); + var chapterTitle = FindTitleInNavigation(navigation, chapterRef.FilePath) + ?? Path.GetFileNameWithoutExtension(chapterRef.FilePath) + ?? $"Chapter {chapterIndex + 1}"; - var paragraphs = ExtractParagraphs(chapter.Content); + var chapterContent = await chapterRef.ReadContentAsTextAsync(); + + var blocks = new List(); + int totalWordCount = 0; + int blockCounter = 0; + + var paragraphs = ExtractParagraphs(chapterContent); foreach (var p in paragraphs) { var sanitizedContent = SanitizeParagraph(p); @@ -99,7 +100,7 @@ public class EpubService : IEpubService blocks.Add(CreateAiTrigger($"trigger-{blockCounter++}")); } - return Result.Ok(new ReaderPageViewModel(blocks, chapterIndex, book.ReadingOrder.Count, chapterTitle)); + return Result.Ok(new ReaderPageViewModel(blocks, chapterIndex, readingOrder.Count, chapterTitle)); } catch (Exception ex) { @@ -162,4 +163,25 @@ public class EpubService : IEpubService new List { "Podsumuj", "Generuj Quiz", "PomiƄ" } ); } + + private string? FindTitleInNavigation(IEnumerable navigation, string? filePath) + { + if (string.IsNullOrEmpty(filePath)) return null; + + var fileName = Path.GetFileName(filePath); + + foreach (var item in navigation) + { + // Match by full path or just filename as fallback + if (item.Link?.ContentFilePath == filePath || item.Link?.ContentFilePath == fileName) + return item.Title; + + if (item.NestedItems != null && item.NestedItems.Any()) + { + var childTitle = FindTitleInNavigation(item.NestedItems, filePath); + if (childTitle != null) return childTitle; + } + } + return null; + } } diff --git a/src/NexusReader.UI.Shared/Components/Atoms/NexusIcon.razor b/src/NexusReader.UI.Shared/Components/Atoms/NexusIcon.razor index cbbb043..4909fe2 100644 --- a/src/NexusReader.UI.Shared/Components/Atoms/NexusIcon.razor +++ b/src/NexusReader.UI.Shared/Components/Atoms/NexusIcon.razor @@ -40,6 +40,12 @@ case "eye-off": break; + case "arrow-left": + + break; + case "arrow-right": + + break; default: diff --git a/src/NexusReader.UI.Shared/Components/Molecules/AiAssistantBubble.razor b/src/NexusReader.UI.Shared/Components/Molecules/AiAssistantBubble.razor index d5eeb1e..48b3e82 100644 --- a/src/NexusReader.UI.Shared/Components/Molecules/AiAssistantBubble.razor +++ b/src/NexusReader.UI.Shared/Components/Molecules/AiAssistantBubble.razor @@ -42,6 +42,7 @@ /// Fallback static dialogue shown when no live AI content is available. [Parameter] public string Dialogue { get; set; } = string.Empty; [Parameter] public List Actions { get; set; } = new(); + [Parameter] public string FullPageContent { get; set; } = string.Empty; [Parameter] public EventCallback OnActionTriggered { get; set; } private string _displayedText = string.Empty; @@ -76,8 +77,11 @@ try { - _packet = await Coordinator.RequestSummaryAndQuizAsync( - $"[ID: {ContextBlockId}]\n{Dialogue}"); + var contentToAnalyze = !string.IsNullOrWhiteSpace(FullPageContent) + ? FullPageContent + : $"[ID: {ContextBlockId}]\n{Dialogue}"; + + _packet = await Coordinator.RequestSummaryAndQuizAsync(contentToAnalyze); var summary = _packet?.Summary; diff --git a/src/NexusReader.UI.Shared/Components/Molecules/IntelligenceToolbar.razor b/src/NexusReader.UI.Shared/Components/Molecules/IntelligenceToolbar.razor index 40da5ff..0c3f270 100644 --- a/src/NexusReader.UI.Shared/Components/Molecules/IntelligenceToolbar.razor +++ b/src/NexusReader.UI.Shared/Components/Molecules/IntelligenceToolbar.razor @@ -42,7 +42,7 @@ @code { protected override void OnInitialized() { - FocusMode.OnFocusModeChanged += StateHasChanged; + FocusMode.OnFocusModeChanged += HandleUpdate; } private async Task HandleClearCache() @@ -56,8 +56,10 @@ } } + private Task HandleUpdate() => InvokeAsync(StateHasChanged); + public void Dispose() { - FocusMode.OnFocusModeChanged -= StateHasChanged; + FocusMode.OnFocusModeChanged -= HandleUpdate; } } diff --git a/src/NexusReader.UI.Shared/Components/Molecules/KnowledgeCheck.razor b/src/NexusReader.UI.Shared/Components/Molecules/KnowledgeCheck.razor index 2c648df..184b464 100644 --- a/src/NexusReader.UI.Shared/Components/Molecules/KnowledgeCheck.razor +++ b/src/NexusReader.UI.Shared/Components/Molecules/KnowledgeCheck.razor @@ -55,12 +55,14 @@ protected override void OnInitialized() { - QuizService.OnQuizUpdated += () => InvokeAsync(StateHasChanged); + QuizService.OnQuizUpdated += HandleUpdate; } + private Task HandleUpdate() => InvokeAsync(StateHasChanged); + public void Dispose() { - QuizService.OnQuizUpdated -= StateHasChanged; + QuizService.OnQuizUpdated -= HandleUpdate; } private async Task SelectOptionAsync(QuizQuestionDto question, int index) diff --git a/src/NexusReader.UI.Shared/Components/Molecules/SelectionAiPanel.razor b/src/NexusReader.UI.Shared/Components/Molecules/SelectionAiPanel.razor index b7f4513..629f109 100644 --- a/src/NexusReader.UI.Shared/Components/Molecules/SelectionAiPanel.razor +++ b/src/NexusReader.UI.Shared/Components/Molecules/SelectionAiPanel.razor @@ -29,7 +29,7 @@
- +
} else @@ -39,7 +39,7 @@
- +
} @@ -76,7 +76,11 @@ private async Task RequestSummary() { IsLoading = true; - Packet = await Coordinator.RequestSummaryAndQuizAsync(SelectedText); + var contextPrompt = !string.IsNullOrWhiteSpace(FullPageContent) + ? $"ANALYSIS CONTEXT (Full Page Content):\n{FullPageContent}\n\nUSER SELECTION TO SUMMARIZE:\n" + : ""; + + Packet = await Coordinator.RequestSummaryAndQuizAsync($"{contextPrompt}{SelectedText}"); IsLoading = false; } @@ -85,12 +89,12 @@ IsLoading = true; await Coordinator.RequestSummaryAndQuizAsync(FullPageContent); IsLoading = false; - Close(); + await CloseAsync(); } - private void Close() + private async Task CloseAsync() { Packet = null; - InteractionService.NotifyTextSelected(string.Empty, string.Empty, null!); + await InteractionService.NotifyTextSelected(string.Empty, string.Empty, null!); } } diff --git a/src/NexusReader.UI.Shared/Components/Organisms/KnowledgeGraph.razor b/src/NexusReader.UI.Shared/Components/Organisms/KnowledgeGraph.razor index a010a46..879093a 100644 --- a/src/NexusReader.UI.Shared/Components/Organisms/KnowledgeGraph.razor +++ b/src/NexusReader.UI.Shared/Components/Organisms/KnowledgeGraph.razor @@ -46,7 +46,7 @@ GraphService.OnLoadingChanged += HandleLoadingChange; } - private async void HandleGraphUpdate() + private async Task HandleGraphUpdate() { if (_module == null) return; @@ -62,13 +62,13 @@ await InvokeAsync(StateHasChanged); } - private async void HandleActiveNodeChange(string nodeId) + private async Task HandleActiveNodeChange(string nodeId) { if (_module == null) return; await _module.InvokeVoidAsync("setActiveNode", nodeId); } - private async void HandleLoadingChange(bool isLoading) + private async Task HandleLoadingChange(bool isLoading) { await InvokeAsync(StateHasChanged); } @@ -81,7 +81,7 @@ if (GraphService.CurrentGraphData != null) { - HandleGraphUpdate(); + await HandleGraphUpdate(); } } } @@ -100,7 +100,7 @@ [JSInvokable] public async Task OnNodeClicked(string nodeId) { - InteractionService.NotifyNodeSelected(nodeId); + await InteractionService.NotifyNodeSelected(nodeId); if (OnNodeSelected.HasDelegate) { @@ -109,7 +109,7 @@ } - private async void HandleFocusSimulation() + private async Task HandleFocusSimulation() { if (_module == null) return; try diff --git a/src/NexusReader.UI.Shared/Components/Organisms/KnowledgeGraph.razor.css b/src/NexusReader.UI.Shared/Components/Organisms/KnowledgeGraph.razor.css index ed59fd1..5e7ea58 100644 --- a/src/NexusReader.UI.Shared/Components/Organisms/KnowledgeGraph.razor.css +++ b/src/NexusReader.UI.Shared/Components/Organisms/KnowledgeGraph.razor.css @@ -98,3 +98,13 @@ filter: drop-shadow(0 0 12px var(--nexus-neon)); 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; +} diff --git a/src/NexusReader.UI.Shared/Components/Organisms/ReaderCanvas.razor b/src/NexusReader.UI.Shared/Components/Organisms/ReaderCanvas.razor index 92b461d..d6cd288 100644 --- a/src/NexusReader.UI.Shared/Components/Organisms/ReaderCanvas.razor +++ b/src/NexusReader.UI.Shared/Components/Organisms/ReaderCanvas.razor @@ -52,15 +52,16 @@ private bool _isJsInitialized; private ElementReference _containerRef; - protected override void OnInitialized() + protected override async Task OnInitializedAsync() { - Coordinator.Clear(); - ThemeService.OnThemeChanged += StateHasChanged; + await Coordinator.ClearAsync(); + ThemeService.OnThemeChanged += HandleUpdate; NavigationService.OnNavigationChanged += OnNavigationChanged; InteractionService.OnScrollToBlockRequested += HandleScrollRequested; InteractionService.OnHighlightBlockRequested += HandleHighlightRequested; InteractionService.OnTextSelected += HandleTextSelected; + SyncService.OnProgressReceived += HandleSyncProgressReceived; } protected override async Task OnParametersSetAsync() @@ -113,60 +114,56 @@ } [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) - _ = 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, // or just log it. Usually, we should prompt the user. 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, - // but we can assume incoming syncs are from other active devices) - _ = InvokeAsync(async () => { - await ScrollToNodeAsync(blockId); - StateHasChanged(); - }); + await ScrollToNodeAsync(blockId); + await InvokeAsync(StateHasChanged); } [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}"); _selectedText = text; _selectedBlockId = blockId; _selectionCoords = coords; - StateHasChanged(); + await InvokeAsync(StateHasChanged); } [JSInvokable] - public void HandleSelectionCleared() + public async Task HandleSelectionCleared() { _selectedText = string.Empty; _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; - StateHasChanged(); + await InvokeAsync(StateHasChanged); await Task.Delay(3000); // Highlight for 3 seconds if (_highlightedBlockId == blockId) { _highlightedBlockId = null; - StateHasChanged(); + await InvokeAsync(StateHasChanged); } } @@ -212,9 +209,11 @@ catch { } } + private Task HandleUpdate() => InvokeAsync(StateHasChanged); + public void Dispose() { - ThemeService.OnThemeChanged -= StateHasChanged; + ThemeService.OnThemeChanged -= HandleUpdate; NavigationService.OnNavigationChanged -= OnNavigationChanged; InteractionService.OnScrollToBlockRequested -= HandleScrollRequested; diff --git a/src/NexusReader.UI.Shared/Components/Organisms/ReaderCanvas.razor.css b/src/NexusReader.UI.Shared/Components/Organisms/ReaderCanvas.razor.css index b718676..5d59a86 100644 --- a/src/NexusReader.UI.Shared/Components/Organisms/ReaderCanvas.razor.css +++ b/src/NexusReader.UI.Shared/Components/Organisms/ReaderCanvas.razor.css @@ -1,16 +1,47 @@ .reader-canvas { - max-width: 800px; - margin: 0 auto; - padding: 2rem 1rem; + width: 100%; + height: 100%; + overflow-y: auto; + overflow-x: hidden; + padding: 2rem 0; transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); 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 { + max-width: 800px; + margin: 0 auto; display: flex; flex-direction: column; gap: 1.5rem; position: relative; + padding: 0 1.5rem 15rem 1.5rem; /* Large padding-bottom for reachability */ } .block-wrapper { @@ -20,6 +51,68 @@ 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 { background: rgba(0, 243, 255, 0.08); box-shadow: 0 0 20px rgba(0, 243, 255, 0.15); diff --git a/src/NexusReader.UI.Shared/Components/Organisms/ReaderFooter.razor b/src/NexusReader.UI.Shared/Components/Organisms/ReaderFooter.razor index 9576000..f2ec3dd 100644 --- a/src/NexusReader.UI.Shared/Components/Organisms/ReaderFooter.razor +++ b/src/NexusReader.UI.Shared/Components/Organisms/ReaderFooter.razor @@ -36,10 +36,9 @@ NavigationService.OnNavigationChanged += HandleNavigationChanged; } - private Task HandleNavigationChanged() + private async Task HandleNavigationChanged() { - StateHasChanged(); - return Task.CompletedTask; + await InvokeAsync(StateHasChanged); } private int CalculateProgress() diff --git a/src/NexusReader.UI.Shared/Components/Organisms/ReaderFooter.razor.css b/src/NexusReader.UI.Shared/Components/Organisms/ReaderFooter.razor.css index 8a403d9..edfa764 100644 --- a/src/NexusReader.UI.Shared/Components/Organisms/ReaderFooter.razor.css +++ b/src/NexusReader.UI.Shared/Components/Organisms/ReaderFooter.razor.css @@ -18,9 +18,11 @@ } .navigation-controls { - display: flex; + display: grid; + grid-template-columns: 32px 1fr 32px; align-items: center; - gap: 1rem; + gap: 0.75rem; + width: 260px; flex-shrink: 0; } @@ -51,18 +53,23 @@ .chapter-info { display: flex; + flex-direction: column; align-items: center; - gap: 0.5rem; - font-size: 0.85rem; + justify-content: center; + min-width: 0; + overflow: hidden; color: #333; - white-space: nowrap; } .chapter-title { font-weight: 600; - max-width: 180px; + font-size: 0.75rem; + max-width: 100%; overflow: hidden; text-overflow: ellipsis; + white-space: nowrap; + text-align: center; + line-height: 1.2; } .chapter-count { diff --git a/src/NexusReader.UI.Shared/Layout/MainLayout.razor b/src/NexusReader.UI.Shared/Layout/MainLayout.razor index 406afaf..2092401 100644 --- a/src/NexusReader.UI.Shared/Layout/MainLayout.razor +++ b/src/NexusReader.UI.Shared/Layout/MainLayout.razor @@ -77,8 +77,8 @@ protected override void OnInitialized() { - FocusMode.OnFocusModeChanged += StateHasChanged; - QuizService.OnQuizUpdated += StateHasChanged; + FocusMode.OnFocusModeChanged += HandleUpdate; + QuizService.OnQuizUpdated += HandleUpdate; var context = PlatformService.GetDeviceContext(); if (context.IsSuccess) @@ -115,9 +115,11 @@ } } + private Task HandleUpdate() => InvokeAsync(StateHasChanged); + public void Dispose() { - FocusMode.OnFocusModeChanged -= StateHasChanged; - QuizService.OnQuizUpdated -= StateHasChanged; + FocusMode.OnFocusModeChanged -= HandleUpdate; + QuizService.OnQuizUpdated -= HandleUpdate; } } diff --git a/src/NexusReader.UI.Shared/Layout/MainLayout.razor.css b/src/NexusReader.UI.Shared/Layout/MainLayout.razor.css index b876b85..4d8621b 100644 --- a/src/NexusReader.UI.Shared/Layout/MainLayout.razor.css +++ b/src/NexusReader.UI.Shared/Layout/MainLayout.razor.css @@ -20,9 +20,10 @@ main { flex: 1; - overflow-y: auto; - overflow-x: hidden; + overflow: hidden; position: relative; + display: flex; + flex-direction: column; } .intelligence-sidebar { diff --git a/src/NexusReader.UI.Shared/Pages/Home.razor b/src/NexusReader.UI.Shared/Pages/Home.razor index e91ead2..e8d1b94 100644 --- a/src/NexusReader.UI.Shared/Pages/Home.razor +++ b/src/NexusReader.UI.Shared/Pages/Home.razor @@ -22,8 +22,8 @@ protected override async Task OnInitializedAsync() { - QuizState.OnQuizRequested += HandleQuizRequested; - FocusMode.OnFocusModeChanged += StateHasChanged; + QuizState.OnQuizRequested += HandleQuizRequestedAsync; + FocusMode.OnFocusModeChanged += HandleUpdate; await FocusMode.InitializeAsync(); } @@ -54,16 +54,18 @@ } } - private void HandleQuizRequested(string blockId) + private async Task HandleQuizRequestedAsync(string blockId) { _activeQuizBlockId = blockId; - StateHasChanged(); + await InvokeAsync(StateHasChanged); } + private Task HandleUpdate() => InvokeAsync(StateHasChanged); + public async ValueTask DisposeAsync() { - QuizState.OnQuizRequested -= HandleQuizRequested; - FocusMode.OnFocusModeChanged -= StateHasChanged; + QuizState.OnQuizRequested -= HandleQuizRequestedAsync; + FocusMode.OnFocusModeChanged -= HandleUpdate; if (_interopModule != null && _keydownHandler != null) { diff --git a/src/NexusReader.UI.Shared/Pages/Home.razor.css b/src/NexusReader.UI.Shared/Pages/Home.razor.css index cded173..ccf7cd2 100644 --- a/src/NexusReader.UI.Shared/Pages/Home.razor.css +++ b/src/NexusReader.UI.Shared/Pages/Home.razor.css @@ -1,8 +1,8 @@ .home-reader-container { width: 100%; + height: 100%; max-width: 800px; margin: 0 auto; - padding: 2rem; transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); } diff --git a/src/NexusReader.UI.Shared/Pages/NotFound.razor b/src/NexusReader.UI.Shared/Pages/NotFound.razor index f74a7a3..05abe35 100644 --- a/src/NexusReader.UI.Shared/Pages/NotFound.razor +++ b/src/NexusReader.UI.Shared/Pages/NotFound.razor @@ -1,5 +1,10 @@ -ï»ż@page "/not-found" -@layout MainLayout - -

Not Found

-

Sorry, the content you are looking for does not exist.

\ No newline at end of file +@page "/not-found" +@layout Layout.MainLayout + +
+
+ +
+
+ Synchronizowanie przestrzeni Nexus... +
\ No newline at end of file diff --git a/src/NexusReader.UI.Shared/Pages/NotFound.razor.css b/src/NexusReader.UI.Shared/Pages/NotFound.razor.css new file mode 100644 index 0000000..98aee16 --- /dev/null +++ b/src/NexusReader.UI.Shared/Pages/NotFound.razor.css @@ -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; } +} diff --git a/src/NexusReader.UI.Shared/Routes.razor b/src/NexusReader.UI.Shared/Routes.razor index e0a5155..91e5272 100644 --- a/src/NexusReader.UI.Shared/Routes.razor +++ b/src/NexusReader.UI.Shared/Routes.razor @@ -9,6 +9,9 @@ + + + diff --git a/src/NexusReader.UI.Shared/Services/FocusModeService.cs b/src/NexusReader.UI.Shared/Services/FocusModeService.cs index 744be38..a21c4d1 100644 --- a/src/NexusReader.UI.Shared/Services/FocusModeService.cs +++ b/src/NexusReader.UI.Shared/Services/FocusModeService.cs @@ -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? 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 { diff --git a/src/NexusReader.UI.Shared/Services/IFocusModeService.cs b/src/NexusReader.UI.Shared/Services/IFocusModeService.cs index 2318632..dbe146e 100644 --- a/src/NexusReader.UI.Shared/Services/IFocusModeService.cs +++ b/src/NexusReader.UI.Shared/Services/IFocusModeService.cs @@ -3,7 +3,7 @@ namespace NexusReader.UI.Shared.Services; public interface IFocusModeService { bool IsFocusModeActive { get; } - event Action? OnFocusModeChanged; + event Func? OnFocusModeChanged; Task InitializeAsync(); Task ToggleAsync(); } diff --git a/src/NexusReader.UI.Shared/Services/IKnowledgeGraphService.cs b/src/NexusReader.UI.Shared/Services/IKnowledgeGraphService.cs index 6db8c10..42f479b 100644 --- a/src/NexusReader.UI.Shared/Services/IKnowledgeGraphService.cs +++ b/src/NexusReader.UI.Shared/Services/IKnowledgeGraphService.cs @@ -8,12 +8,12 @@ public interface IKnowledgeGraphService string? ActiveNodeId { get; } bool IsLoading { get; } - event Action? OnGraphUpdated; - event Action? OnActiveNodeChanged; - event Action? OnLoadingChanged; + event Func? OnGraphUpdated; + event Func? OnActiveNodeChanged; + event Func? 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(); } diff --git a/src/NexusReader.UI.Shared/Services/IQuizStateService.cs b/src/NexusReader.UI.Shared/Services/IQuizStateService.cs index 571719e..52fd657 100644 --- a/src/NexusReader.UI.Shared/Services/IQuizStateService.cs +++ b/src/NexusReader.UI.Shared/Services/IQuizStateService.cs @@ -9,11 +9,11 @@ public interface IQuizStateService bool IsHydrating { get; } bool HasNewQuiz { get; } - event Action? OnQuizRequested; - event Action? OnQuizUpdated; + event Func? OnQuizRequested; + event Func? 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(); } diff --git a/src/NexusReader.UI.Shared/Services/IReaderInteractionService.cs b/src/NexusReader.UI.Shared/Services/IReaderInteractionService.cs index c8ead52..2b65fa9 100644 --- a/src/NexusReader.UI.Shared/Services/IReaderInteractionService.cs +++ b/src/NexusReader.UI.Shared/Services/IReaderInteractionService.cs @@ -2,15 +2,15 @@ namespace NexusReader.UI.Shared.Services; public interface IReaderInteractionService { - event Action? OnNodeSelected; - event Action? OnScrollToBlockRequested; - event Action? OnHighlightBlockRequested; - event Action? OnTextSelected; + event Func? OnNodeSelected; + event Func? OnScrollToBlockRequested; + event Func? OnHighlightBlockRequested; + event Func? 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); diff --git a/src/NexusReader.UI.Shared/Services/ISyncService.cs b/src/NexusReader.UI.Shared/Services/ISyncService.cs index 1bae5ce..35d1529 100644 --- a/src/NexusReader.UI.Shared/Services/ISyncService.cs +++ b/src/NexusReader.UI.Shared/Services/ISyncService.cs @@ -6,6 +6,6 @@ public interface ISyncService { Task InitializeAsync(); Task UpdateProgressAsync(string pageId); - event Action OnProgressReceived; + event Func OnProgressReceived; Task DisposeAsync(); } diff --git a/src/NexusReader.UI.Shared/Services/IThemeService.cs b/src/NexusReader.UI.Shared/Services/IThemeService.cs index 84a8409..1b20eff 100644 --- a/src/NexusReader.UI.Shared/Services/IThemeService.cs +++ b/src/NexusReader.UI.Shared/Services/IThemeService.cs @@ -3,6 +3,6 @@ namespace NexusReader.UI.Shared.Services; public interface IThemeService { bool IsLightMode { get; } - event Action? OnThemeChanged; - void ToggleTheme(); + event Func? OnThemeChanged; + Task ToggleTheme(); } diff --git a/src/NexusReader.UI.Shared/Services/KnowledgeCoordinator.cs b/src/NexusReader.UI.Shared/Services/KnowledgeCoordinator.cs index 223409b..eb90650 100644 --- a/src/NexusReader.UI.Shared/Services/KnowledgeCoordinator.cs +++ b/src/NexusReader.UI.Shared/Services/KnowledgeCoordinator.cs @@ -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 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); } diff --git a/src/NexusReader.UI.Shared/Services/KnowledgeGraphService.cs b/src/NexusReader.UI.Shared/Services/KnowledgeGraphService.cs index a96b85b..dc0bc70 100644 --- a/src/NexusReader.UI.Shared/Services/KnowledgeGraphService.cs +++ b/src/NexusReader.UI.Shared/Services/KnowledgeGraphService.cs @@ -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? OnActiveNodeChanged; - public event Action? OnLoadingChanged; + public event Func? OnGraphUpdated; + public event Func? OnActiveNodeChanged; + public event Func? 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(); } } diff --git a/src/NexusReader.UI.Shared/Services/QuizStateService.cs b/src/NexusReader.UI.Shared/Services/QuizStateService.cs index 81a3435..c4cef8b 100644 --- a/src/NexusReader.UI.Shared/Services/QuizStateService.cs +++ b/src/NexusReader.UI.Shared/Services/QuizStateService.cs @@ -9,34 +9,34 @@ public sealed class QuizStateService : IQuizStateService public bool IsHydrating { get; private set; } public bool HasNewQuiz { get; private set; } - public event Action? OnQuizRequested; - public event Action? OnQuizUpdated; + public event Func? OnQuizRequested; + public event Func? 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(); } } diff --git a/src/NexusReader.UI.Shared/Services/ReaderInteractionService.cs b/src/NexusReader.UI.Shared/Services/ReaderInteractionService.cs index e26e045..56865cb 100644 --- a/src/NexusReader.UI.Shared/Services/ReaderInteractionService.cs +++ b/src/NexusReader.UI.Shared/Services/ReaderInteractionService.cs @@ -2,28 +2,28 @@ namespace NexusReader.UI.Shared.Services; public sealed class ReaderInteractionService : IReaderInteractionService { - public event Action? OnNodeSelected; - public event Action? OnScrollToBlockRequested; - public event Action? OnHighlightBlockRequested; - public event Action? OnTextSelected; + public event Func? OnNodeSelected; + public event Func? OnScrollToBlockRequested; + public event Func? OnHighlightBlockRequested; + public event Func? 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); } } diff --git a/src/NexusReader.UI.Shared/Services/SyncService.cs b/src/NexusReader.UI.Shared/Services/SyncService.cs index 66d3dd6..8fb515d 100644 --- a/src/NexusReader.UI.Shared/Services/SyncService.cs +++ b/src/NexusReader.UI.Shared/Services/SyncService.cs @@ -14,7 +14,7 @@ public class SyncService : ISyncService, IAsyncDisposable private bool _isInitialized; private CancellationTokenSource? _debounceCts; - public event Action? OnProgressReceived; + public event Func? OnProgressReceived; public SyncService( HttpClient httpClient, @@ -44,9 +44,9 @@ public class SyncService : ISyncService, IAsyncDisposable .WithAutomaticReconnect() .Build(); - _hubConnection.On("ProgressUpdated", (pageId, timestamp) => + _hubConnection.On("ProgressUpdated", async (pageId, timestamp) => { - OnProgressReceived?.Invoke(pageId, timestamp); + if (OnProgressReceived != null) await OnProgressReceived(pageId, timestamp); }); try diff --git a/src/NexusReader.UI.Shared/Services/ThemeService.cs b/src/NexusReader.UI.Shared/Services/ThemeService.cs index 6a4e695..2223346 100644 --- a/src/NexusReader.UI.Shared/Services/ThemeService.cs +++ b/src/NexusReader.UI.Shared/Services/ThemeService.cs @@ -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? OnThemeChanged; - public void ToggleTheme() + public async Task ToggleTheme() { IsLightMode = !IsLightMode; - OnThemeChanged?.Invoke(); + if (OnThemeChanged != null) await OnThemeChanged(); } } diff --git a/src/NexusReader.UI.Shared/wwwroot/js/knowledgeGraph.js b/src/NexusReader.UI.Shared/wwwroot/js/knowledgeGraph.js index 362490d..8119ff0 100644 --- a/src/NexusReader.UI.Shared/wwwroot/js/knowledgeGraph.js +++ b/src/NexusReader.UI.Shared/wwwroot/js/knowledgeGraph.js @@ -134,9 +134,10 @@ export function updateData(data) { .attr("fill", "none") .attr("stroke-width", d => d.relationType === 'Defines' ? 2 : 1) .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, - exit => exit.remove() + exit => exit.transition().duration(500).style("opacity", 0).remove() ); // Update Nodes @@ -146,8 +147,9 @@ export function updateData(data) { .join( enter => { const g = enter.append("g") - .attr("class", "node-group") + .attr("class", "node-group neon-flash-node") .style("cursor", "pointer") + .style("opacity", 0) .on("click", (e, d) => { currentDotNetHelper.invokeMethodAsync('OnNodeClicked', d.id); setActiveNode(d.id); @@ -162,8 +164,7 @@ export function updateData(data) { if (d.type === 'Rule') return '#ff4444'; return "url(#nebulaGlow)"; }) - .attr("opacity", 0) - .transition().duration(1000).attr("opacity", d => d.group === 'current' ? 0.6 : 0.2); + .attr("opacity", d => d.group === 'current' ? 0.6 : 0.2); g.append("rect") .attr("class", "node-pill") @@ -187,10 +188,12 @@ export function updateData(data) { .attr("fill", d => d.type === 'Definition' ? 'var(--nexus-accent)' : '#ccc') .attr("font-size", "0.8rem"); + g.transition().duration(500).style("opacity", 1); + return g; }, - update => update, - exit => exit.remove() + update => update.classed("neon-flash-node", false), + exit => exit.transition().duration(500).style("opacity", 0).remove() ); simulation.nodes(data.nodes); @@ -223,7 +226,11 @@ export function setActiveNode(nodeId) { if (!svgElement || !node) return; 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(); @@ -235,6 +242,9 @@ export function setActiveNode(nodeId) { badge.style("display", "block").datum(d); badge.attr("transform", `translate(${d.x},${d.y})`); + // Dim others + dimNodes(nodeId); + // Smooth transition svgElement.transition().duration(1000).call( 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) { if (simulation) { simulation.stop(); diff --git a/src/NexusReader.Web.Client/Program.cs b/src/NexusReader.Web.Client/Program.cs index 059823e..56034d7 100644 --- a/src/NexusReader.Web.Client/Program.cs +++ b/src/NexusReader.Web.Client/Program.cs @@ -4,6 +4,9 @@ using NexusReader.Application.Abstractions.Services; using NexusReader.Web.Client.Services; using NexusReader.UI.Shared.Services; using NexusReader.Application; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.AI; +using NexusReader.Data.Persistence; var builder = WebAssemblyHostBuilder.CreateDefault(args); @@ -32,7 +35,24 @@ builder.Services.AddCascadingAuthenticationState(); builder.Services.AddScoped(); 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>(new ThrowingDbContextFactory()); +builder.Services.AddSingleton>>(new ThrowingEmbeddingGenerator()); + builder.Services.AddApplication(); builder.Services.AddScoped(); await builder.Build().RunAsync(); + +public class ThrowingDbContextFactory : IDbContextFactory +{ + public AppDbContext CreateDbContext() => throw new NotSupportedException("DbContext cannot be used in WASM client."); +} + +public class ThrowingEmbeddingGenerator : IEmbeddingGenerator> +{ + public void Dispose() { } + public Task>> GenerateAsync(IEnumerable 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; +}