From f18663426bb681153f3e08d80a04ac9fb0e6fd2b Mon Sep 17 00:00:00 2001 From: Antigravity Date: Fri, 5 Jun 2026 09:51:29 +0000 Subject: [PATCH] style(ui): refactor reader layout grid, fix focus mode layout collapse, fix SVG rendering dots, reorganize intelligence toolbar (#69) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reorganized the reader toolbar and layout grid to improve visual consistency and layout robustness in Focus Mode. Fixed outline SVG rendering bugs that caused icons to show as solid dots. Closes #70 --------- Co-authored-by: Marek Jasiński Reviewed-on: https://git.archimap.cloud/mjasin/Nexus.Reader/pulls/69 Co-authored-by: Antigravity Co-committed-by: Antigravity --- README.md | 10 + .../Components/Atoms/NexusIcon.razor | 109 +++-- .../Components/Molecules/CalloutBox.razor | 47 ++ .../Components/Molecules/CalloutBox.razor.css | 122 +++++ .../Molecules/IntelligenceToolbar.razor | 63 ++- .../Molecules/IntelligenceToolbar.razor.css | 61 ++- .../Molecules/KnowledgeCheck.razor.css | 172 +++++++ .../Molecules/SelectionAiPanel.razor | 236 +++++++--- .../Molecules/SelectionAiPanel.razor.css | 243 +++++----- .../Organisms/BookIngestionModal.razor | 2 +- .../Organisms/BookIngestionModal.razor.css | 92 ++-- .../Components/Organisms/KnowledgeGraph.razor | 34 +- .../Organisms/KnowledgeGraph.razor.css | 52 ++- .../Components/Organisms/ReaderCanvas.razor | 21 +- .../Organisms/ReaderCanvas.razor.css | 141 +++++- .../Organisms/ReaderFooter.razor.css | 133 ++++-- .../Layout/ReaderLayout.razor | 83 +++- .../Layout/ReaderLayout.razor.css | 428 ++++++++++++++++-- .../Models/ReaderModels.cs | 2 +- .../Services/KnowledgeCoordinator.cs | 55 +++ src/NexusReader.UI.Shared/wwwroot/app.css | 255 +++++++++-- .../wwwroot/js/knowledgeGraph.js | 102 ++--- .../wwwroot/js/selectionHandler.js | 127 +++++- src/NexusReader.Web/Program.cs | 3 +- 24 files changed, 2022 insertions(+), 571 deletions(-) create mode 100644 src/NexusReader.UI.Shared/Components/Molecules/CalloutBox.razor create mode 100644 src/NexusReader.UI.Shared/Components/Molecules/CalloutBox.razor.css diff --git a/README.md b/README.md index fb573bc..3f8ac51 100644 --- a/README.md +++ b/README.md @@ -36,3 +36,13 @@ Run test suite: ```bash dotnet test --no-restore ``` + +## 🗄️ Database Migrations + +Automatic database migrations at startup (`MigrateAsync()`) have been disabled to ensure compatibility with Native AOT compilation and prevent locking issues in multi-instance environments. + +To apply database migrations locally, run the EF Core migration command from the solution root: + +```bash +dotnet ef database update --project src/NexusReader.Infrastructure --startup-project src/NexusReader.Web +``` diff --git a/src/NexusReader.UI.Shared/Components/Atoms/NexusIcon.razor b/src/NexusReader.UI.Shared/Components/Atoms/NexusIcon.razor index 68dd356..b571496 100644 --- a/src/NexusReader.UI.Shared/Components/Atoms/NexusIcon.razor +++ b/src/NexusReader.UI.Shared/Components/Atoms/NexusIcon.razor @@ -2,106 +2,129 @@ @switch (Name.ToLowerInvariant()) { case "home": - - + + break; case "map": - - - + + + break; case "share": case "share-2": - - - - - + + + + + break; case "help-circle": - - - + + + break; case "robot": - + break; case "play": - + break; case "check": - + break; case "search": - + + break; case "message-square": - + break; case "diamond": - + break; case "layout": - - - + + + break; case "book": case "book-open": - - + + break; case "user": - - + + break; case "settings": - + + + break; case "bookmark": - + break; case "target": - + + + break; case "trash": - + + + + break; case "mail": - + + break; case "lock": - + + break; case "eye": - + + break; case "eye-off": - + + + + break; case "arrow-left": - + + break; case "arrow-right": - + + break; case "log-out": - + break; case "chevron-left": - + break; case "chevron-right": - + break; case "x": case "close": - - + + + break; + case "sun": + + + break; + case "moon": + break; default: - - + + break; } diff --git a/src/NexusReader.UI.Shared/Components/Molecules/CalloutBox.razor b/src/NexusReader.UI.Shared/Components/Molecules/CalloutBox.razor new file mode 100644 index 0000000..81492eb --- /dev/null +++ b/src/NexusReader.UI.Shared/Components/Molecules/CalloutBox.razor @@ -0,0 +1,47 @@ +@namespace NexusReader.UI.Shared.Components.Molecules + +
+ @if (!string.IsNullOrEmpty(Title)) + { +
+ @if (Type == CalloutType.Warning || Type == CalloutType.Error) + { + + } + else if (Type == CalloutType.Success) + { + + } + else + { + + } + @Title +
+ } +
+ @ChildContent +
+
+ +@code { + public enum CalloutType + { + Info, + Warning, + Success, + Error + } + + [Parameter] + public CalloutType Type { get; set; } = CalloutType.Info; + + [Parameter] + public string? Title { get; set; } + + [Parameter] + public RenderFragment? ChildContent { get; set; } + + [Parameter] + public string Class { get; set; } = string.Empty; +} diff --git a/src/NexusReader.UI.Shared/Components/Molecules/CalloutBox.razor.css b/src/NexusReader.UI.Shared/Components/Molecules/CalloutBox.razor.css new file mode 100644 index 0000000..b3cd993 --- /dev/null +++ b/src/NexusReader.UI.Shared/Components/Molecules/CalloutBox.razor.css @@ -0,0 +1,122 @@ +.nexus-callout-box { + padding: 1rem 1.25rem; + margin: 1.5rem 0 1.5rem 0; + border-radius: 0 8px 8px 0; + font-family: var(--nexus-font-sans, sans-serif); + font-size: 0.95rem; + line-height: 1.5; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + transition: all 0.2s ease; + border: 1px solid transparent; + border-left-width: 4px; +} + +/* Light / Dark default support via variables or custom colors */ +.nexus-callout-box { + background-color: rgba(255, 255, 255, 0.02); + color: #e2e8f0; +} + +/* Info style */ +.nexus-callout-info { + border-left-color: var(--nexus-neon, #00ff99); +} + +/* Warning style */ +.nexus-callout-warning { + border-left-color: #eab308; /* warning yellow */ + background-color: rgba(234, 179, 8, 0.03); +} + +/* Success style */ +.nexus-callout-success { + border-left-color: #10b981; /* success green */ + background-color: rgba(16, 185, 129, 0.03); +} + +/* Error style */ +.nexus-callout-error { + border-left-color: #f43f5e; /* error red */ + background-color: rgba(244, 63, 94, 0.03); +} + +.nexus-callout-header { + display: flex; + align-items: center; + gap: 0.5rem; + font-weight: 600; + margin-bottom: 0.5rem; + text-transform: uppercase; + font-size: 0.8rem; + letter-spacing: 0.05em; +} + +.nexus-callout-info .nexus-callout-header { + color: var(--nexus-neon, #00ff99); +} + +.nexus-callout-warning .nexus-callout-header { + color: #eab308; +} + +.nexus-callout-success .nexus-callout-header { + color: #10b981; +} + +.nexus-callout-error .nexus-callout-header { + color: #f43f5e; +} + +.nexus-callout-icon { + flex-shrink: 0; +} + +.nexus-callout-body { + opacity: 0.9; +} + +/* Light theme support */ +.theme-light .nexus-callout-box { + background-color: #fcfcfb; + border: 1px solid rgba(0, 0, 0, 0.03); + border-left-width: 4px; + color: #44403c; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.015); +} + +.theme-light .nexus-callout-info { + border-left-color: #10b981; + background-color: rgba(16, 185, 129, 0.04); +} + +.theme-light .nexus-callout-info .nexus-callout-header { + color: #059669; +} + +.theme-light .nexus-callout-warning { + border-left-color: #d97706; + background-color: rgba(217, 119, 6, 0.04); +} + +.theme-light .nexus-callout-warning .nexus-callout-header { + color: #d97706; +} + +.theme-light .nexus-callout-success { + border-left-color: #10b981; + background-color: rgba(16, 185, 129, 0.04); +} + +.theme-light .nexus-callout-success .nexus-callout-header { + color: #059669; +} + +.theme-light .nexus-callout-error { + border-left-color: #e11d48; + background-color: rgba(225, 29, 72, 0.04); +} + +.theme-light .nexus-callout-error .nexus-callout-header { + color: #e11d48; +} + diff --git a/src/NexusReader.UI.Shared/Components/Molecules/IntelligenceToolbar.razor b/src/NexusReader.UI.Shared/Components/Molecules/IntelligenceToolbar.razor index 8fad5e7..840acdc 100644 --- a/src/NexusReader.UI.Shared/Components/Molecules/IntelligenceToolbar.razor +++ b/src/NexusReader.UI.Shared/Components/Molecules/IntelligenceToolbar.razor @@ -1,45 +1,43 @@ @using NexusReader.UI.Shared.Services @using NexusReader.Application.Abstractions.Services @inject IFocusModeService FocusMode -@inject IKnowledgeService KnowledgeService @inject IIdentityService IdentityService @inject NavigationManager NavigationManager +@inject IThemeService ThemeService +@inject IKnowledgeService KnowledgeService +@implements IDisposable @@ -48,11 +46,11 @@ protected override void OnInitialized() { FocusMode.OnFocusModeChanged += HandleUpdate; + ThemeService.OnThemeChanged += HandleThemeChangedAsync; } private async Task HandleClearCache() { - // For now, a simple console log confirm or just do it Console.WriteLine("[IntelligenceToolbar] Requesting cache clear..."); var result = await KnowledgeService.ClearCacheAsync(); if (result.IsSuccess) @@ -61,16 +59,13 @@ } } - private async Task HandleLogout() - { - await IdentityService.LogoutAsync(); - NavigationManager.NavigateTo("/account/logout-form", true); - } - private Task HandleUpdate() => InvokeAsync(StateHasChanged); + private Task HandleThemeChangedAsync() => InvokeAsync(StateHasChanged); + public void Dispose() { FocusMode.OnFocusModeChanged -= HandleUpdate; + ThemeService.OnThemeChanged -= HandleThemeChangedAsync; } } diff --git a/src/NexusReader.UI.Shared/Components/Molecules/IntelligenceToolbar.razor.css b/src/NexusReader.UI.Shared/Components/Molecules/IntelligenceToolbar.razor.css index a5fc31e..a0abe27 100644 --- a/src/NexusReader.UI.Shared/Components/Molecules/IntelligenceToolbar.razor.css +++ b/src/NexusReader.UI.Shared/Components/Molecules/IntelligenceToolbar.razor.css @@ -71,26 +71,53 @@ transform: rotate(180deg); } -.toolbar-item.danger:hover { - color: #ff4d4d; - background: rgba(255, 77, 77, 0.1); + + +.toolbar-separator { + width: 24px; + height: 1px; + background: rgba(255, 255, 255, 0.08); + margin: 0.2rem 0; } -.toolbar-item.logout-item { - margin-top: 1rem; - border-top: 1px solid rgba(255, 255, 255, 0.08); - padding-top: 1.5rem; - height: auto; - width: 100%; - display: flex; - justify-content: center; - border-radius: 0; - color: #444; +/* Light mode overrides */ +.theme-light .intelligence-toolbar { + background: #f5f5f4; + border-right: 1px solid rgba(0, 0, 0, 0.08); + box-shadow: inset -2px 0 10px rgba(0, 0, 0, 0.02); } -.toolbar-item.logout-item:hover { - color: #ff4d4d; - background: none; - filter: drop-shadow(0 0 8px rgba(255, 77, 77, 0.4)); +.theme-light .toolbar-item { + color: #78716c; } +.theme-light .toolbar-item:hover { + color: #10b981; + background: rgba(16, 185, 129, 0.05); + box-shadow: 0 0 10px rgba(16, 185, 129, 0.1); + filter: none; +} + +.theme-light .toolbar-item.active { + color: #10b981; + background: rgba(16, 185, 129, 0.08); + box-shadow: 0 0 15px rgba(16, 185, 129, 0.15); + filter: none; +} + +.theme-light .toolbar-item.active::after { + background: #10b981; + box-shadow: none; +} + +.theme-light .toolbar-item.focus-active { + color: #10b981; + filter: none; +} + +.theme-light .toolbar-separator { + background: rgba(0, 0, 0, 0.08); +} + + + diff --git a/src/NexusReader.UI.Shared/Components/Molecules/KnowledgeCheck.razor.css b/src/NexusReader.UI.Shared/Components/Molecules/KnowledgeCheck.razor.css index 1194c53..2407015 100644 --- a/src/NexusReader.UI.Shared/Components/Molecules/KnowledgeCheck.razor.css +++ b/src/NexusReader.UI.Shared/Components/Molecules/KnowledgeCheck.razor.css @@ -335,3 +335,175 @@ box-shadow: none; } +/* Light mode overrides */ +.theme-light .knowledge-check { + background: #fafaf9; + border: 1px solid rgba(0, 0, 0, 0.08); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.02); +} + +.theme-light .header-title { + color: #1c1917; +} + +.theme-light .question-text { + color: #44403c; +} + +.theme-light .option-item { + background: #ffffff; + border: 1px solid rgba(0, 0, 0, 0.08); +} + +.theme-light .option-item:hover { + background: #f5f5f4; +} + +.theme-light .option-item.selected { + border-color: #10b981; + background: rgba(16, 185, 129, 0.04); +} + +.theme-light .option-letter { + color: #059669; +} + +.theme-light .option-text { + color: #292524; +} + +.theme-light .option-correct { + border-color: #10b981 !important; + background: rgba(16, 185, 129, 0.08) !important; +} + +.theme-light .option-incorrect { + border-color: #f43f5e !important; + background: rgba(244, 63, 94, 0.08) !important; +} + +.theme-light .option-revealed-correct { + border-color: #10b981 !important; + background: rgba(16, 185, 129, 0.06) !important; + box-shadow: 0 0 8px rgba(16, 185, 129, 0.1); +} + +.theme-light .loading-state.shimmer { + background: linear-gradient(90deg, transparent, rgba(0, 0, 0, 0.03), transparent); + color: #10b981; + text-shadow: none; +} + +.theme-light .submit-btn { + background: rgba(0, 0, 0, 0.03); + border: 1px solid rgba(0, 0, 0, 0.15); + color: #78716c; +} + +.theme-light .submit-btn:not(:disabled) { + background: #10b981; + color: #ffffff; + border-color: #10b981; +} + +.theme-light .submitted-title { + color: #1c1917; +} + +.theme-light .submitted-text { + color: #78716c; +} + +.theme-light .score-card { + background: #ffffff; + border: 1px solid rgba(0, 0, 0, 0.06); + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.02); +} + +.theme-light .score-num { + color: #10b981; + text-shadow: none; +} + +.theme-light .score-divider { + color: #e7e5e4; +} + +.theme-light .score-total { + color: #292524; +} + +.theme-light .score-percent { + color: #78716c; +} + +.theme-light .reset-quiz-btn { + border: 1px solid rgba(0, 0, 0, 0.15); + color: #44403c; +} + +.theme-light .reset-quiz-btn:hover { + background: rgba(0, 0, 0, 0.03); + border-color: #1c1917; + box-shadow: 0 0 15px rgba(0, 0, 0, 0.03); +} + +.theme-light .empty-title { + color: #1c1917; +} + +.theme-light .empty-text { + color: #78716c; +} + +.theme-light .empty-icon-wrapper { + background: rgba(16, 185, 129, 0.02); + border: 1px solid rgba(16, 185, 129, 0.1); + box-shadow: 0 0 20px rgba(16, 185, 129, 0.02); +} + +.theme-light .empty-quiz-state:hover .empty-icon-wrapper { + background: rgba(16, 185, 129, 0.06); + border-color: rgba(16, 185, 129, 0.3); + box-shadow: 0 0 25px rgba(16, 185, 129, 0.1); +} + +.theme-light .generate-quiz-btn { + background: rgba(16, 185, 129, 0.05); + border: 1px solid #10b981; + color: #10b981; + text-shadow: none; + box-shadow: 0 0 15px rgba(16, 185, 129, 0.05); +} + +.theme-light .generate-quiz-btn:not(:disabled):hover { + background: #10b981; + color: #ffffff; + box-shadow: 0 0 25px rgba(16, 185, 129, 0.2); + transform: translateY(-2px); +} + +.theme-light .generate-quiz-btn:disabled { + border-color: rgba(0, 0, 0, 0.1); + background: rgba(0, 0, 0, 0.02); + color: #a8a29e; + box-shadow: none; +} + +.theme-light .success-icon-wrapper { + background: rgba(16, 185, 129, 0.05); + border: 1px solid rgba(16, 185, 129, 0.2); + box-shadow: 0 0 20px rgba(16, 185, 129, 0.08); +} + +.theme-light .success-glow { + color: #10b981; + filter: none; +} + +.theme-light .neon-glow { + color: #10b981; + filter: none; +} + + diff --git a/src/NexusReader.UI.Shared/Components/Molecules/SelectionAiPanel.razor b/src/NexusReader.UI.Shared/Components/Molecules/SelectionAiPanel.razor index 9c10153..d0c600e 100644 --- a/src/NexusReader.UI.Shared/Components/Molecules/SelectionAiPanel.razor +++ b/src/NexusReader.UI.Shared/Components/Molecules/SelectionAiPanel.razor @@ -3,49 +3,47 @@ @using NexusReader.Application.DTOs.AI @inject KnowledgeCoordinator Coordinator @inject IReaderInteractionService InteractionService +@inject IQuizStateService QuizService +@inject IJSRuntime JS @if (IsVisible) { -
-
-
-
- -
- E-Czytnik - Asystent AI -
-
-
- @if (IsLoading) - { -
-
Skanowanie fragmentu...
-
- } - else if (Packet != null) - { -
- @Packet.Summary -
-
- - -
- } - else - { -
- Wykryto ciekawy fragment! Czy chcesz, abym wygenerował podsumowanie lub quiz z tego rozdziału? -
-
- - -
- } -
-
-
+
+ +
+
} @@ -56,47 +54,145 @@ [Parameter] public string FullPageContent { get; set; } = string.Empty; private bool IsVisible => !string.IsNullOrEmpty(SelectedText) && Coordinates != null; - private bool IsLoading = false; - private KnowledgePacket? Packet; - private bool PositionBelow => Coordinates != null && Coordinates.Top < 320; + private bool IsLoadingSummary = false; + private bool IsLoadingQuiz = false; + private bool IsAnyLoading => IsLoadingSummary || IsLoadingQuiz; + + private string _style = "visibility: hidden; opacity: 0; pointer-events: none;"; + private bool _positionBelow = false; + private SelectionCoordinates? _lastCoordinates; protected override void OnParametersSet() { - Console.WriteLine($"[SelectionAiPanel] Parameters set. SelectedText: {SelectedText.Length} chars, Coordinates: {Coordinates?.Top}, PositionBelow: {PositionBelow}"); - // Reset packet when selection changes - Packet = null; - } - - private string PanelStyle => Coordinates != null - ? string.Create(System.Globalization.CultureInfo.InvariantCulture, - $"top: {(PositionBelow ? Coordinates.Top + 35 : Coordinates.Top - 15):F1}px !important; " + - $"left: {Math.Clamp(Coordinates.Left + Coordinates.Width / 2, 280, 1600):F1}px !important; " + - $"transform: translate(-50%, {(PositionBelow ? "0" : "-100%")}) !important;") - : ""; - - private async Task RequestSummary() - { - IsLoading = true; - var contextPrompt = !string.IsNullOrWhiteSpace(FullPageContent) - ? $"ANALYSIS CONTEXT (Full Page Content):\n{FullPageContent}\n\nUSER SELECTION TO SUMMARIZE:\n" - : ""; + Console.WriteLine($"[SelectionAiPanel] Parameters set. SelectedText: {SelectedText.Length} chars, Coordinates: {Coordinates?.Top}"); - var result = await Coordinator.RequestSummaryAndQuizAsync($"{contextPrompt}{SelectedText}"); - Packet = result.IsSuccess ? result.Value : null; - IsLoading = false; + if (Coordinates != _lastCoordinates) + { + _lastCoordinates = Coordinates; + _style = "visibility: hidden; opacity: 0; pointer-events: none;"; + _positionBelow = false; + } + + // Reset loading states when parameters change + IsLoadingSummary = false; + IsLoadingQuiz = false; } - private async Task GenerateFullQuiz() + protected override async Task OnAfterRenderAsync(bool firstRender) { - IsLoading = true; - await Coordinator.RequestSummaryAndQuizAsync(FullPageContent); - IsLoading = false; - await CloseAsync(); + if (IsVisible && _style.Contains("visibility: hidden")) + { + try + { + var module = await JS.InvokeAsync("import", "./_content/NexusReader.UI.Shared/js/selectionHandler.js"); + var result = await module.InvokeAsync("positionToolbar"); + if (result != null) + { + _style = string.Create(System.Globalization.CultureInfo.InvariantCulture, + $"left: {result.Left:F1}px !important; " + + $"top: {result.Top:F1}px !important; " + + $"visibility: visible !important; " + + $"opacity: 1 !important; " + + $"pointer-events: auto !important;"); + _positionBelow = result.Below; + StateHasChanged(); + } + } + catch (Exception ex) + { + Console.WriteLine($"[SelectionAiPanel] Error positioning toolbar: {ex.Message}"); + } + } + } + + private async Task RequestSummaryAsync() + { + if (IsAnyLoading) return; + IsLoadingSummary = true; + StateHasChanged(); + + try + { + var module = await JS.InvokeAsync("import", "./_content/NexusReader.UI.Shared/js/selectionHandler.js"); + var selectedText = await module.InvokeAsync("getSelectionText"); + if (string.IsNullOrWhiteSpace(selectedText)) + { + selectedText = SelectedText; + } + + if (!string.IsNullOrWhiteSpace(selectedText)) + { + var contextPrompt = !string.IsNullOrWhiteSpace(FullPageContent) + ? $"ANALYSIS CONTEXT (Full Page Content):\n{FullPageContent}\n\nUSER SELECTION TO SUMMARIZE:\n" + : ""; + + _ = Coordinator.StartSelectionSummaryAsync($"{contextPrompt}{selectedText}"); + await CloseAsync(); + await InteractionService.RequestAssistant(); + } + } + catch (Exception ex) + { + Console.WriteLine($"[SelectionAiPanel] Error requesting summary: {ex.Message}"); + } + finally + { + IsLoadingSummary = false; + StateHasChanged(); + } + } + + private async Task GenerateQuizAsync() + { + if (IsAnyLoading) return; + IsLoadingQuiz = true; + StateHasChanged(); + + try + { + var module = await JS.InvokeAsync("import", "./_content/NexusReader.UI.Shared/js/selectionHandler.js"); + var selectedText = await module.InvokeAsync("getSelectionText"); + if (string.IsNullOrWhiteSpace(selectedText)) + { + selectedText = SelectedText; + } + + if (!string.IsNullOrWhiteSpace(selectedText)) + { + var contextPrompt = !string.IsNullOrWhiteSpace(FullPageContent) + ? $"ANALYSIS CONTEXT (Full Page Content):\n{FullPageContent}\n\nUSER SELECTION TO SUMMARIZE:\n" + : ""; + + var result = await Coordinator.RequestSummaryAndQuizAsync($"{contextPrompt}{selectedText}"); + if (result.IsSuccess) + { + await CloseAsync(); + await QuizService.RequestQuiz(BlockId); + } + } + } + catch (Exception ex) + { + Console.WriteLine($"[SelectionAiPanel] Error generating quiz: {ex.Message}"); + } + finally + { + IsLoadingQuiz = false; + StateHasChanged(); + } } private async Task CloseAsync() { - Packet = null; await InteractionService.NotifyTextSelected(string.Empty, string.Empty, null!); } + + private class PositionResult + { + public double Left { get; set; } + public double Top { get; set; } + public bool Below { get; set; } + } } + + diff --git a/src/NexusReader.UI.Shared/Components/Molecules/SelectionAiPanel.razor.css b/src/NexusReader.UI.Shared/Components/Molecules/SelectionAiPanel.razor.css index f99e021..b87eaae 100644 --- a/src/NexusReader.UI.Shared/Components/Molecules/SelectionAiPanel.razor.css +++ b/src/NexusReader.UI.Shared/Components/Molecules/SelectionAiPanel.razor.css @@ -1,158 +1,149 @@ .selection-ai-panel { - position: fixed; - z-index: 9999; - width: 550px; - max-width: 90vw; - animation: fadeInScale 0.2s ease-out; - pointer-events: auto; + position: absolute; + z-index: 10000; + display: flex; + align-items: center; + background: rgba(24, 24, 28, 0.85); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5), 0 12px 28px rgba(0, 0, 0, 0.4); + padding: 4px 6px; + gap: 4px; + pointer-events: none; /* Controlled by inline styles */ + user-select: none; + animation: fadeInScale 0.18s cubic-bezier(0.16, 1, 0.3, 1); } @keyframes fadeInScale { - from { opacity: 0; transform: translate(-50%, -90%) scale(0.95); } - to { opacity: 1; transform: translate(-50%, -100%) scale(1); } + from { + opacity: 0; + transform: scale(0.96); + } + to { + opacity: 1; + transform: scale(1); + } } -.ai-bubble { - position: relative; - display: flex; - flex-direction: row; - gap: 1.5rem; - padding: 1.5rem; - background: rgba(18, 18, 18, 0.95); - backdrop-filter: blur(10px); - border: 1px solid rgba(255, 255, 255, 0.1); - border-radius: 16px; - box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3); - color: #fff; +.selection-ai-panel.below { + animation: fadeInScaleBelow 0.18s cubic-bezier(0.16, 1, 0.3, 1); } -.ai-avatar { +@keyframes fadeInScaleBelow { + from { + opacity: 0; + transform: scale(0.96); + } + to { + opacity: 1; + transform: scale(1); + } +} + +.toolbar-btn { display: flex; - flex-direction: column; align-items: center; - gap: 0.5rem; - min-width: 100px; -} - -.avatar-label { - display: flex; - flex-direction: column; - align-items: center; - text-align: center; -} - -.avatar-label .name { + gap: 6px; + padding: 6px 12px; + background: transparent; + border: none; + border-radius: 6px; + color: #e4e4e7; /* zinc-200 */ font-size: 0.8rem; - font-weight: 600; - color: #fff; -} - -.avatar-label .role { - font-size: 0.7rem; - opacity: 0.6; -} - -.neon-pulse { - color: #00ff99; - filter: drop-shadow(0 0 8px #00ff99); - animation: pulse 2s infinite ease-in-out; -} - -@keyframes pulse { - 0% { transform: scale(1); filter: drop-shadow(0 0 8px #00ff99); } - 50% { transform: scale(1.05); filter: drop-shadow(0 0 15px #00ff99); } - 100% { transform: scale(1); filter: drop-shadow(0 0 8px #00ff99); } -} - -.ai-content { - display: flex; - flex-direction: column; - gap: 1rem; - justify-content: center; -} - -.summary-box { - font-size: 0.95rem; - line-height: 1.5; - color: #e0e0e0; - max-height: 40vh; - overflow-y: auto; - padding-right: 8px; -} - -.summary-box::-webkit-scrollbar { - width: 4px; -} - -.summary-box::-webkit-scrollbar-thumb { - background: rgba(0, 255, 153, 0.3); - border-radius: 2px; -} - -.ai-actions { - display: flex; - gap: 1rem; -} - -.action-btn { - padding: 0.5rem 1.2rem; - border-radius: 20px; - font-size: 0.85rem; + font-weight: 500; cursor: pointer; - transition: all 0.2s ease; + transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); + white-space: nowrap; font-family: inherit; } -.action-btn.ghost { - background: transparent; - border: 1px solid rgba(255, 255, 255, 0.2); - color: #aaa; +.toolbar-btn:hover:not(.disabled) { + background: rgba(255, 255, 255, 0.05); + color: #ffffff; } -.action-btn.neon-border { - background: rgba(0, 255, 153, 0.1); - border: 1px solid #00ff99; - color: #00ff99; +.toolbar-btn.primary { + color: var(--nexus-neon, #00ff99); } -.action-btn:hover { - transform: translateY(-2px); - box-shadow: 0 4px 12px rgba(0, 255, 153, 0.2); +.toolbar-btn.primary:hover:not(.disabled) { + background: rgba(0, 255, 153, 0.08); + box-shadow: 0 0 12px rgba(0, 255, 153, 0.15); } -.bubble-pointer { - position: absolute; - left: 50%; - transform: translateX(-50%); - width: 0; - height: 0; - border-left: 10px solid transparent; - border-right: 10px solid transparent; +.toolbar-btn.disabled { + opacity: 0.35; + cursor: not-allowed; + pointer-events: none; } -.selection-ai-panel:not(.below) .bubble-pointer { - bottom: -10px; - border-top: 10px solid rgba(18, 18, 18, 0.95); +.toolbar-divider { + width: 1px; + height: 16px; + background: rgba(255, 255, 255, 0.1); } -.selection-ai-panel.below .bubble-pointer { - top: -10px; - border-bottom: 10px solid rgba(18, 18, 18, 0.95); +.btn-icon { + display: inline-flex; + align-items: center; + justify-content: center; + flex-shrink: 0; } -.loading-state { - padding: 1rem; +.spinner-inline { + width: 12px; + height: 12px; + border: 2px solid currentColor; + border-top-color: transparent; + border-radius: 50%; + animation: spin 0.8s linear infinite; + display: inline-block; + flex-shrink: 0; } -.shimmer { - background: linear-gradient(90deg, transparent, rgba(0, 255, 153, 0.2), transparent); - background-size: 200% 100%; - animation: shimmer 1.5s infinite; - padding: 0.5rem; - border-radius: 4px; +@keyframes spin { + to { + transform: rotate(360deg); + } } -@keyframes shimmer { - from { background-position: 200% 0; } - to { background-position: -200% 0; } +.opacity-50 { + opacity: 0.5 !important; } + +.cursor-not-allowed { + cursor: not-allowed !important; +} + +/* Light mode overrides */ +.theme-light .selection-ai-panel { + background: rgba(254, 254, 254, 0.95); + border: 1px solid rgba(0, 0, 0, 0.08); + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.05), 0 10px 30px rgba(0, 0, 0, 0.04); +} + +.theme-light .toolbar-btn { + color: #57524e; +} + +.theme-light .toolbar-btn:hover:not(.disabled) { + background: rgba(0, 0, 0, 0.04); + color: #1c1917; +} + +.theme-light .toolbar-btn.primary { + color: #10b981; +} + +.theme-light .toolbar-btn.primary:hover:not(.disabled) { + background: rgba(16, 185, 129, 0.06); + box-shadow: 0 0 12px rgba(16, 185, 129, 0.1); +} + +.theme-light .toolbar-divider { + background: rgba(0, 0, 0, 0.08); +} + + diff --git a/src/NexusReader.UI.Shared/Components/Organisms/BookIngestionModal.razor b/src/NexusReader.UI.Shared/Components/Organisms/BookIngestionModal.razor index d2abe30..d749f75 100644 --- a/src/NexusReader.UI.Shared/Components/Organisms/BookIngestionModal.razor +++ b/src/NexusReader.UI.Shared/Components/Organisms/BookIngestionModal.razor @@ -41,7 +41,7 @@
-
+
@if (Metadata != null) {
diff --git a/src/NexusReader.UI.Shared/Components/Organisms/BookIngestionModal.razor.css b/src/NexusReader.UI.Shared/Components/Organisms/BookIngestionModal.razor.css index d92d068..0eebfbf 100644 --- a/src/NexusReader.UI.Shared/Components/Organisms/BookIngestionModal.razor.css +++ b/src/NexusReader.UI.Shared/Components/Organisms/BookIngestionModal.razor.css @@ -196,52 +196,37 @@ margin-top: 1rem; } -.btn { - font-family: var(--nexus-font-sans); - font-weight: 600; - padding: 0.75rem 1.5rem; - border-radius: 8px; - border: 1px solid transparent; - cursor: pointer; - transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); - font-size: 0.85rem; - letter-spacing: 0.5px; - display: inline-flex; - align-items: center; - justify-content: center; - text-transform: uppercase; +::deep .nexus-btn.btn-primary { + background: var(--nexus-neon, #00ffaa) !important; + color: #050505 !important; + border-color: transparent !important; + box-shadow: 0 4px 12px rgba(var(--nexus-accent-rgb, 0, 255, 170), 0.2) !important; } -.btn-primary { - background: var(--nexus-neon, #00ffaa); - color: #050505; - box-shadow: 0 4px 12px rgba(var(--nexus-accent-rgb, 0, 255, 170), 0.2); +::deep .nexus-btn.btn-primary:hover:not(:disabled) { + background: #00e699 !important; + transform: translateY(-2px) !important; + box-shadow: 0 6px 20px rgba(var(--nexus-accent-rgb, 0, 255, 170), 0.4) !important; } -.btn-primary:hover { - background: #00e699; - transform: translateY(-2px); - box-shadow: 0 6px 20px rgba(var(--nexus-accent-rgb, 0, 255, 170), 0.4); +::deep .nexus-btn.btn-primary:active:not(:disabled) { + transform: translateY(0) !important; } -.btn-primary:active { - transform: translateY(0); +::deep .nexus-btn.btn-secondary { + background: rgba(255, 255, 255, 0.03) !important; + color: var(--nexus-text) !important; + border: 1px solid rgba(255, 255, 255, 0.1) !important; } -.btn-secondary { - background: rgba(255, 255, 255, 0.03); - color: var(--nexus-text); - border: 1px solid rgba(255, 255, 255, 0.1); +::deep .nexus-btn.btn-secondary:hover:not(:disabled) { + background: rgba(255, 255, 255, 0.08) !important; + border-color: rgba(255, 255, 255, 0.3) !important; + transform: translateY(-2px) !important; } -.btn-secondary:hover { - background: rgba(255, 255, 255, 0.08); - border-color: rgba(255, 255, 255, 0.3); - transform: translateY(-2px); -} - -.btn-secondary:active { - transform: translateY(0); +::deep .nexus-btn.btn-secondary:active:not(:disabled) { + transform: translateY(0) !important; } /* Verification State */ @@ -357,27 +342,30 @@ to { transform: scale(1.2); opacity: 0.8; } } -.btn:disabled { - opacity: 0.5; - cursor: not-allowed; - filter: grayscale(1); +::deep .nexus-btn:disabled:not(.btn-loading) { + opacity: 0.4 !important; + cursor: not-allowed !important; + filter: grayscale(1) !important; } -.btn-loading { - position: relative; +::deep .nexus-btn.btn-loading { + position: relative !important; color: transparent !important; + opacity: 1 !important; + cursor: wait !important; + filter: none !important; } -.btn-loading::after { - content: ""; - position: absolute; - width: 20px; - height: 20px; - border: 2px solid rgba(255, 255, 255, 0.2); - border-top-color: var(--nexus-neon, #00ffaa); - border-radius: 50%; - animation: spin 0.8s linear infinite; - filter: drop-shadow(0 0 4px var(--nexus-neon, #00ffaa)); +::deep .nexus-btn.btn-loading::after { + content: "" !important; + position: absolute !important; + width: 20px !important; + height: 20px !important; + border: 2px solid rgba(255, 255, 255, 0.2) !important; + border-top-color: var(--nexus-neon, #00ffaa) !important; + border-radius: 50% !important; + animation: spin 0.8s linear infinite !important; + filter: drop-shadow(0 0 4px var(--nexus-neon, #00ffaa)) !important; } /* Indexing State */ diff --git a/src/NexusReader.UI.Shared/Components/Organisms/KnowledgeGraph.razor b/src/NexusReader.UI.Shared/Components/Organisms/KnowledgeGraph.razor index e709228..8e9c499 100644 --- a/src/NexusReader.UI.Shared/Components/Organisms/KnowledgeGraph.razor +++ b/src/NexusReader.UI.Shared/Components/Organisms/KnowledgeGraph.razor @@ -12,21 +12,21 @@
@if (GraphService.IsLoading || GraphService.CurrentGraphData == null) { -
-
- -
+
+
+ +
+
+ Mapowanie relacji rozdziału...
- Mapowanie relacji rozdziału... -
} else { -
- - - -
+
+ + + +
}
@@ -49,7 +49,7 @@ private async Task HandleGraphUpdate() { if (_module == null) return; - + if (GraphService.CurrentGraphData == null) { await _module.InvokeVoidAsync("clear"); @@ -58,7 +58,7 @@ { await _module.InvokeVoidAsync("updateData", GraphService.CurrentGraphData); } - + await InvokeAsync(StateHasChanged); } @@ -78,7 +78,7 @@ if (firstRender) { await InitializeGraphAsync(); - + if (GraphService.CurrentGraphData != null) { await HandleGraphUpdate(); @@ -101,7 +101,7 @@ public async Task OnNodeClicked(string nodeId) { await InteractionService.NotifyNodeSelected(nodeId); - + if (OnNodeSelected.HasDelegate) { await OnNodeSelected.InvokeAsync(nodeId); @@ -128,7 +128,7 @@ GraphService.OnGraphUpdated -= HandleGraphUpdate; GraphService.OnActiveNodeChanged -= HandleActiveNodeChange; GraphService.OnLoadingChanged -= HandleLoadingChange; - + try { if (_module is not null) @@ -138,7 +138,7 @@ } } catch { } - + _dotNetHelper?.Dispose(); } } diff --git a/src/NexusReader.UI.Shared/Components/Organisms/KnowledgeGraph.razor.css b/src/NexusReader.UI.Shared/Components/Organisms/KnowledgeGraph.razor.css index 761e103..0fb0d26 100644 --- a/src/NexusReader.UI.Shared/Components/Organisms/KnowledgeGraph.razor.css +++ b/src/NexusReader.UI.Shared/Components/Organisms/KnowledgeGraph.razor.css @@ -10,7 +10,7 @@ position: relative; } -.knowledge-graph-container.loading > ::deep svg { +.knowledge-graph-container.loading> ::deep svg { display: none !important; } @@ -93,9 +93,20 @@ } @keyframes robot-pulse { - 0% { transform: scale(1); filter: drop-shadow(0 0 10px var(--nexus-neon)); } - 50% { transform: scale(1.1); filter: drop-shadow(0 0 25px var(--nexus-neon)); } - 100% { transform: scale(1); filter: drop-shadow(0 0 10px var(--nexus-neon)); } + 0% { + transform: scale(1); + filter: drop-shadow(0 0 10px var(--nexus-neon)); + } + + 50% { + transform: scale(1.1); + filter: drop-shadow(0 0 25px var(--nexus-neon)); + } + + 100% { + transform: scale(1); + filter: drop-shadow(0 0 10px var(--nexus-neon)); + } } .scan-line { @@ -111,9 +122,17 @@ } @keyframes scan { - 0% { top: 0; } - 50% { top: 100%; } - 100% { top: 0; } + 0% { + top: 0; + } + + 50% { + top: 100%; + } + + 100% { + top: 0; + } } ::deep .nexus-node-active { @@ -124,11 +143,24 @@ } ::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)); } + 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; } + +.knowledge-graph-container ::deep svg { + background: radial-gradient(circle, #1a1a1a 0%, #121212 100%); + transition: background 0.3s ease; +} \ No newline at end of file diff --git a/src/NexusReader.UI.Shared/Components/Organisms/ReaderCanvas.razor b/src/NexusReader.UI.Shared/Components/Organisms/ReaderCanvas.razor index 98d2323..736442a 100644 --- a/src/NexusReader.UI.Shared/Components/Organisms/ReaderCanvas.razor +++ b/src/NexusReader.UI.Shared/Components/Organisms/ReaderCanvas.razor @@ -99,6 +99,7 @@ private bool _isMobile = false; private DotNetObjectReference? _selfReference; private IJSObjectReference? _viewportModule; + private IJSObjectReference? _selectionModule; protected override async Task OnInitializedAsync() { @@ -201,10 +202,13 @@ { try { - var module = await JS.InvokeAsync("import", "./_content/NexusReader.UI.Shared/js/selectionHandler.js"); + if (_selectionModule == null) + { + _selectionModule = await JS.InvokeAsync("import", "./_content/NexusReader.UI.Shared/js/selectionHandler.js"); + } if (_selfReference != null) { - await module.InvokeVoidAsync("initSelectionListener", _selfReference, _containerRef); + await _selectionModule.InvokeVoidAsync("initSelectionListener", _selfReference, _containerRef); } } catch (Exception ex) @@ -440,6 +444,19 @@ InteractionService.OnTextSelected -= HandleTextSelected; SyncService.OnProgressReceived -= HandleSyncProgressReceived; + try + { + if (_selectionModule != null) + { + await _selectionModule.InvokeVoidAsync("destroySelectionListener"); + await _selectionModule.DisposeAsync(); + } + } + catch (Exception ex) + { + Logger.LogDebug(ex, "Failed to destroy JS selection listener."); + } + try { if (_viewportModule != null) diff --git a/src/NexusReader.UI.Shared/Components/Organisms/ReaderCanvas.razor.css b/src/NexusReader.UI.Shared/Components/Organisms/ReaderCanvas.razor.css index fac7f04..6b00afe 100644 --- a/src/NexusReader.UI.Shared/Components/Organisms/ReaderCanvas.razor.css +++ b/src/NexusReader.UI.Shared/Components/Organisms/ReaderCanvas.razor.css @@ -6,7 +6,7 @@ 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; @@ -30,18 +30,40 @@ background-color: rgba(0, 255, 153, 0.5); } +.reader-canvas.theme-dark { + background-color: #121214; +} + .reader-canvas.theme-light { - background-color: #F9F9F9; /* Paper-white requirement */ + background-color: #f4f1ea; + /* Warm light beige/gray background */ } .reader-flow-container { - max-width: 800px; - margin: 0 auto; + max-width: 680px; + margin: 2rem auto; + min-height: calc(100vh - 180px); display: flex; flex-direction: column; gap: 1.5rem; position: relative; - padding: 0 1.5rem 15rem 1.5rem; /* Large padding-bottom for reachability */ + padding: 3rem 4rem 15rem 4rem; + /* Large padding-bottom for reachability, plus comfortable side margins */ + border-radius: 12px; + box-sizing: border-box; + transition: background-color 0.3s, box-shadow 0.3s, border-color 0.3s; +} + +.theme-dark .reader-flow-container { + background-color: #1a1a1e; + border: 1px solid rgba(255, 255, 255, 0.03); + box-shadow: 0 4px 30px rgba(0, 0, 0, 0.2); +} + +.theme-light .reader-flow-container { + background-color: #ffffff; + border: 1px solid rgba(0, 0, 0, 0.04); + box-shadow: 0 4px 20px rgba(139, 130, 115, 0.12); } .block-wrapper { @@ -57,24 +79,49 @@ line-height: 1.65 !important; letter-spacing: -0.01em !important; font-size: 1.15rem; - font-weight: 300; + font-weight: 400; + text-align: left !important; + color: #e4e4e7; + /* Off-white with light gray tint */ } .theme-light ::deep .nexus-ebook { - color: #1a1a1a; + color: #292524; + /* Warm charcoal for legibility */ } +/* Callout Box styling for legacy blockquote segments */ +::deep .nexus-ebook blockquote { + background-color: rgba(255, 255, 255, 0.02); + border-left: 4px solid var(--nexus-neon); + padding: 1rem 1.25rem; + margin: 1.5rem 0 1.5rem 0; + border-radius: 0 8px 8px 0; + font-size: 1.05rem; + color: #e2e8f0; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); +} + +.theme-light ::deep .nexus-ebook blockquote { + background-color: rgba(245, 158, 11, 0.04); + border-left: 4px solid #f59e0b; + color: #44403c; +} + + /* Technical Code Block Container */ ::deep .nexus-ebook pre { - background-color: #2d2d2d; /* Dark theme for code for better contrast */ + 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 */ - + 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; @@ -101,7 +148,8 @@ /* Inline Code Highlight */ ::deep .nexus-ebook p code { background-color: rgba(0, 0, 0, 0.05); - color: #d63384; /* Classic differentiator for inline code */ + color: #d63384; + /* Classic differentiator for inline code */ padding: 0.2rem 0.4rem; border-radius: 4px; font-size: 0.9em; @@ -153,9 +201,20 @@ } @keyframes pulse-small { - 0% { transform: scale(1); opacity: 1; } - 50% { transform: scale(1.1); opacity: 0.8; } - 100% { transform: scale(1); opacity: 1; } + 0% { + transform: scale(1); + opacity: 1; + } + + 50% { + transform: scale(1.1); + opacity: 0.8; + } + + 100% { + transform: scale(1); + opacity: 1; + } } /* Chapter Loading Overlay and Spinners */ @@ -246,29 +305,48 @@ } @keyframes spin { - 0% { transform: rotate(0deg); } - 100% { transform: rotate(360deg); } + 0% { + transform: rotate(0deg); + } + + 100% { + transform: rotate(360deg); + } } @keyframes scaleIn { - from { transform: scale(0.9) translateY(10px); opacity: 0; } - to { transform: scale(1) translateY(0); opacity: 1; } + from { + transform: scale(0.9) translateY(10px); + opacity: 0; + } + + to { + transform: scale(1) translateY(0); + opacity: 1; + } } @keyframes fadeIn { - from { opacity: 0; } - to { opacity: 1; } + from { + opacity: 0; + } + + to { + opacity: 1; + } } /* MOBILE READER UI OVERRIDES */ @media (max-width: 768px) { .reader-canvas { padding-top: 54px !important; - padding-bottom: 80px !important; /* Ensure content is clear of bottom toolbar */ + padding-bottom: 80px !important; + /* Ensure content is clear of bottom toolbar */ } .reader-flow-container { - padding-bottom: 4rem; /* Safe breathing room */ + padding-bottom: 4rem; + /* Safe breathing room */ } } @@ -338,7 +416,20 @@ } .theme-light .nexus-mobile-chapter-title { - color: #1a1a1a; + color: #292524; +} + +.theme-light .nexus-mobile-escape-btn { + color: #78716c; +} + +.theme-light .nexus-mobile-escape-btn:hover { + color: #10b981; + background-color: rgba(16, 185, 129, 0.05); +} + +.theme-light .nexus-mobile-escape-btn:active { + background-color: rgba(16, 185, 129, 0.08); } .nexus-chapter-nav-btn { @@ -372,4 +463,4 @@ .theme-light .nexus-chapter-nav-btn:hover:not(:disabled) { background: rgba(0, 0, 0, 0.06); -} +} \ No newline at end of file diff --git a/src/NexusReader.UI.Shared/Components/Organisms/ReaderFooter.razor.css b/src/NexusReader.UI.Shared/Components/Organisms/ReaderFooter.razor.css index edfa764..9ebb68f 100644 --- a/src/NexusReader.UI.Shared/Components/Organisms/ReaderFooter.razor.css +++ b/src/NexusReader.UI.Shared/Components/Organisms/ReaderFooter.razor.css @@ -1,64 +1,99 @@ .reader-footer { - position: relative; - height: 50px; - background: #F9F9F9; - border-top: 1px solid rgba(0, 0, 0, 0.08); + position: absolute; + bottom: 24px; + left: 50%; + transform: translateX(-50%); + width: min(600px, 90%); + height: 54px; + background: rgba(24, 24, 27, 0.6); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 9999px; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3); display: flex; align-items: center; padding: 0 1.5rem; - z-index: 10; - flex-shrink: 0; + z-index: 100; + transition: background 0.3s, border-color 0.3s, box-shadow 0.3s; +} + +.theme-light .reader-footer { + background: rgba(254, 254, 254, 0.75); + border: 1px solid rgba(0, 0, 0, 0.08); + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.04), 0 1px 3px rgba(0, 0, 0, 0.02); } .footer-content { display: flex; align-items: center; width: 100%; - gap: 1.5rem; + gap: 1rem; + justify-content: space-between; } .navigation-controls { - display: grid; - grid-template-columns: 32px 1fr 32px; + display: flex; align-items: center; gap: 0.75rem; - width: 260px; - flex-shrink: 0; + flex: 1; + min-width: 0; } .nav-btn { - background: white; - border: 1px solid rgba(0, 0, 0, 0.1); - border-radius: 6px; + background: rgba(255, 255, 255, 0.03); + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 50%; width: 32px; height: 32px; display: flex; align-items: center; justify-content: center; cursor: pointer; - transition: all 0.2s ease; - color: #333; - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); + transition: color 0.2s ease-in-out, background-color 0.2s ease-in-out, border-color 0.2s ease-in-out, transform 0.2s ease-in-out; + color: #a1a1aa; /* Zinc-400 default contrast */ } -.nav-btn:hover:not(:disabled) { - background: #f0f0f0; - transform: translateY(-1px); +.nav-btn:hover:not(:disabled), +.nav-btn:focus:not(:disabled) { + background: rgba(255, 255, 255, 0.08); + border-color: var(--nexus-neon, #00ff99); + color: var(--nexus-neon, #00ff99); /* Brand neon green hover/focus signal */ + transform: scale(1.05); + outline: none; } .nav-btn:disabled { - opacity: 0.3; + opacity: 0.25; cursor: not-allowed; } +.theme-light .nav-btn { + background: rgba(0, 0, 0, 0.02); + border-color: rgba(0, 0, 0, 0.08); + color: #78716c; /* Warm stone-500 */ +} + +.theme-light .nav-btn:hover:not(:disabled), +.theme-light .nav-btn:focus:not(:disabled) { + background: rgba(16, 185, 129, 0.05); + border-color: #10b981; + color: #10b981; + outline: none; +} + .chapter-info { display: flex; flex-direction: column; - align-items: center; + align-items: flex-start; justify-content: center; min-width: 0; - overflow: hidden; - color: #333; + flex: 1; + color: #e2e8f0; /* Slate-200 for clean high readability */ +} + +.theme-light .chapter-info { + color: #292524; /* Warm charcoal for legibility */ } .chapter-title { @@ -68,42 +103,66 @@ overflow: hidden; text-overflow: ellipsis; white-space: nowrap; - text-align: center; line-height: 1.2; } .chapter-count { - opacity: 0.5; - font-size: 0.75rem; + color: #a1a1aa; /* Zinc-400 for secondary info clarity */ + font-size: 0.7rem; +} + +.theme-light .chapter-count { + color: #78716c; /* Warm stone-500 secondary info */ } .progress-container { - flex: 1; - height: 6px; - background: rgba(0, 0, 0, 0.05); - border-radius: 3px; + width: 80px; + height: 4px; + background: rgba(255, 255, 255, 0.08); + border-radius: 2px; overflow: hidden; - margin: 0 1rem; + margin: 0 0.25rem; + flex-shrink: 0; +} + +.theme-light .progress-container { + background: rgba(0, 0, 0, 0.08); } .progress-bar { height: 100%; - background: #2ECC71; - border-radius: 3px; + background: var(--nexus-neon, #00ff99); + border-radius: 2px; transition: width 0.3s ease; } +.theme-light .progress-bar { + background: #10b981; +} + .meta-info { display: flex; align-items: center; - gap: 1rem; - font-size: 0.75rem; - color: #888; + gap: 0.75rem; + font-size: 0.7rem; + color: #a1a1aa; flex-shrink: 0; + font-family: monospace; +} + +.theme-light .meta-info { + color: #78716c; } .battery { display: flex; align-items: center; gap: 0.3rem; +} + +/* RWD constraint: floating toolbar visible ONLY on desktop (min-width: 1024px) */ +@media (max-width: 1023px) { + .reader-footer { + display: none !important; + } } \ No newline at end of file diff --git a/src/NexusReader.UI.Shared/Layout/ReaderLayout.razor b/src/NexusReader.UI.Shared/Layout/ReaderLayout.razor index f802a2b..97633c3 100644 --- a/src/NexusReader.UI.Shared/Layout/ReaderLayout.razor +++ b/src/NexusReader.UI.Shared/Layout/ReaderLayout.razor @@ -16,10 +16,12 @@ @inject IIdentityService IdentityService @inject NavigationManager NavigationManager @inject Microsoft.Extensions.Logging.ILogger Logger +@inject IThemeService ThemeService +@inject KnowledgeCoordinator Coordinator @implements IAsyncDisposable -
+
@Body @@ -62,7 +64,32 @@ Contextual Intelligence Panel
- @if (_selectedNode != null) + @if (Coordinator.IsLoadingSelectionSummary) + { +
+
+
+
+
+
+
+ } + else if (!string.IsNullOrEmpty(Coordinator.SelectionSummary)) + { +
+
+
+ PODSUMOWANIE + +
+

Zaznaczony Fragment

+
+
+

@Coordinator.SelectionSummary

+
+
+ } + else if (_selectedNode != null) {
@@ -165,7 +192,32 @@ {
- @if (_selectedNode != null) + @if (Coordinator.IsLoadingSelectionSummary) + { +
+
+
+
+
+
+
+ } + else if (!string.IsNullOrEmpty(Coordinator.SelectionSummary)) + { +
+
+
+ PODSUMOWANIE + +
+

Zaznaczony Fragment

+
+
+

@Coordinator.SelectionSummary

+
+
+ } + else if (_selectedNode != null) {
@@ -291,6 +343,8 @@ InteractionService.OnAssistantRequested += HandleAssistantRequestedAsync; InteractionService.OnScrollPercentChanged += HandleScrollPercentChanged; GraphService.OnGraphUpdated += HandleGraphUpdatedAsync; + ThemeService.OnThemeChanged += HandleThemeChangedAsync; + Coordinator.OnSelectionSummaryStateChanged += HandleUpdate; var context = PlatformService.GetDeviceContext(); if (context.IsSuccess) @@ -305,6 +359,8 @@ } } + private async Task HandleThemeChangedAsync() => await InvokeAsync(StateHasChanged); + private void SetActiveTab(SidebarTab tab) { _activeTab = tab; @@ -329,6 +385,11 @@ StateHasChanged(); } + private async Task ClearSelectionSummary() + { + await Coordinator.ClearSelectionSummaryAsync(); + } + private async Task HandleScrollPercentChanged(int percent) { _scrollPercentage = percent; @@ -349,12 +410,24 @@ { if (_isMobile) { + if (Coordinator.IsLoadingSelectionSummary || !string.IsNullOrEmpty(Coordinator.SelectionSummary)) + { + _activeMobileTab = MobileReaderTab.Concepts; + _activeTab = SidebarTab.Knowledge; + } OpenAssistant(); } else { _activeMobileTab = MobileReaderTab.Concepts; - _activeTab = SidebarTab.Quiz; + if (Coordinator.IsLoadingSelectionSummary || !string.IsNullOrEmpty(Coordinator.SelectionSummary)) + { + _activeTab = SidebarTab.Knowledge; + } + else + { + _activeTab = SidebarTab.Quiz; + } } await InvokeAsync(StateHasChanged); } @@ -445,6 +518,8 @@ InteractionService.OnAssistantRequested -= HandleAssistantRequestedAsync; InteractionService.OnScrollPercentChanged -= HandleScrollPercentChanged; GraphService.OnGraphUpdated -= HandleGraphUpdatedAsync; + ThemeService.OnThemeChanged -= HandleThemeChangedAsync; + Coordinator.OnSelectionSummaryStateChanged -= HandleUpdate; try { diff --git a/src/NexusReader.UI.Shared/Layout/ReaderLayout.razor.css b/src/NexusReader.UI.Shared/Layout/ReaderLayout.razor.css index 7b633c6..ccc78f0 100644 --- a/src/NexusReader.UI.Shared/Layout/ReaderLayout.razor.css +++ b/src/NexusReader.UI.Shared/Layout/ReaderLayout.razor.css @@ -4,18 +4,20 @@ width: 100vw; height: 100vh; overflow: hidden; - background: #121212; + background: var(--nexus-bg); } .reader-pane { - background: #F9F9F9; + grid-column: 1; + background: var(--nexus-bg); position: relative; overflow: hidden; display: flex; flex-direction: column; z-index: 5; height: 100vh; + transition: background 0.2s ease, color 0.2s ease; } main { @@ -27,30 +29,65 @@ main { } .intelligence-sidebar { + grid-column: 3; display: grid; grid-template-columns: 50px 1fr; - width: 100%; /* controlled by grid */ + width: 100%; + /* controlled by grid */ height: 100%; - background: #0d0d0d; + background: var(--nexus-card); box-shadow: -10px 0 30px rgba(0, 0, 0, 0.3); - border-left: 1px solid rgba(255, 255, 255, 0.1); + border-left: 1px solid rgba(255, 255, 255, 0.05); overflow: hidden; z-index: 10; + transition: background 0.2s ease, border-color 0.2s ease; } .resizer { - width: 4px; + grid-column: 2; + width: 12px; cursor: col-resize; - background: rgba(255, 255, 255, 0.02); - transition: background 0.2s, width 0.2s; + background: transparent; z-index: 20; - border-left: 1px solid rgba(255, 255, 255, 0.05); + position: relative; + display: flex; + align-items: center; + justify-content: center; } -.resizer:hover, .app-container.is-resizing .resizer { +.resizer::before { + content: ''; + position: absolute; + left: 50%; + transform: translateX(-50%); + top: 0; + width: 1px; + height: 100%; + background: rgba(255, 255, 255, 0.05); + transition: background 0.2s ease; +} + +.resizer::after { + content: ''; + width: 4px; + height: 60px; + background: rgba(255, 255, 255, 0.15); + border-radius: 9999px; + transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); +} + +.resizer:hover::before, +.app-container.is-resizing .resizer::before { + background: rgba(255, 255, 255, 0.15); +} + +.resizer:hover::after, +.app-container.is-resizing .resizer::after { background: var(--nexus-neon); width: 6px; - box-shadow: 0 0 10px var(--nexus-neon); + height: 80px; + box-shadow: 0 0 12px var(--nexus-neon); } .app-container.is-resizing { @@ -63,6 +100,7 @@ main { } .app-container.focus-mode-active .intelligence-sidebar { + grid-column: 3; grid-template-columns: 50px 0px; } @@ -94,7 +132,7 @@ main { border-bottom: 1px solid rgba(255, 255, 255, 0.05); font-family: var(--nexus-font-sans); font-size: 0.9rem; - color: #fff; + color: var(--nexus-text); flex-shrink: 0; } @@ -149,9 +187,20 @@ main { } @keyframes quiz-pulse { - 0% { filter: drop-shadow(0 0 2px var(--nexus-neon)); transform: scale(1); } - 50% { filter: drop-shadow(0 0 10px var(--nexus-neon)); transform: scale(1.1); } - 100% { filter: drop-shadow(0 0 2px var(--nexus-neon)); transform: scale(1); } + 0% { + filter: drop-shadow(0 0 2px var(--nexus-neon)); + transform: scale(1); + } + + 50% { + filter: drop-shadow(0 0 10px var(--nexus-neon)); + transform: scale(1.1); + } + + 100% { + filter: drop-shadow(0 0 2px var(--nexus-neon)); + transform: scale(1); + } } /* Contextual Intelligence Panel Layout */ @@ -226,9 +275,20 @@ main { } @keyframes glow-pulse { - 0% { transform: scale(0.9); opacity: 0.5; } - 50% { transform: scale(1.1); opacity: 1; } - 100% { transform: scale(0.9); opacity: 0.5; } + 0% { + transform: scale(0.9); + opacity: 0.5; + } + + 50% { + transform: scale(1.1); + opacity: 1; + } + + 100% { + transform: scale(0.9); + opacity: 0.5; + } } .placeholder-text { @@ -245,8 +305,15 @@ main { } @keyframes fade-in { - from { opacity: 0; transform: translateY(5px); } - to { opacity: 1; transform: translateY(0); } + from { + opacity: 0; + transform: translateY(5px); + } + + to { + opacity: 1; + transform: translateY(0); + } } .node-header-section { @@ -432,9 +499,20 @@ main { } @keyframes quiz-pulse-glow { - 0% { border-color: rgba(0, 240, 255, 0.3); box-shadow: 0 0 5px rgba(0, 240, 255, 0.1); } - 50% { border-color: var(--nexus-neon, #00f0ff); box-shadow: 0 0 25px rgba(0, 240, 255, 0.3); } - 100% { border-color: rgba(0, 240, 255, 0.3); box-shadow: 0 0 5px rgba(0, 240, 255, 0.1); } + 0% { + border-color: rgba(0, 240, 255, 0.3); + box-shadow: 0 0 5px rgba(0, 240, 255, 0.1); + } + + 50% { + border-color: var(--nexus-neon, #00f0ff); + box-shadow: 0 0 25px rgba(0, 240, 255, 0.3); + } + + 100% { + border-color: rgba(0, 240, 255, 0.3); + box-shadow: 0 0 5px rgba(0, 240, 255, 0.1); + } } /* Quiz Navigation Header */ @@ -479,7 +557,8 @@ main { .platform-mobile .reader-pane { width: 100vw !important; - height: 100vh !important; /* full viewport height */ + height: 100vh !important; + /* full viewport height */ position: absolute; top: 0; left: 0; @@ -506,9 +585,11 @@ main { } .platform-mobile .nexus-mobile-reader-tabs { - display: none; /* Keep hidden by default */ + display: none; + /* Keep hidden by default */ width: 100vw; - height: 100vh; /* full viewport height */ + height: 100vh; + /* full viewport height */ position: absolute; top: 0; left: 0; @@ -519,7 +600,8 @@ main { .app-container.platform-mobile.active-mobile-tab-graph .nexus-mobile-reader-tabs, .app-container.platform-mobile.active-mobile-tab-concepts .nexus-mobile-reader-tabs { - display: block; /* Show only when graph or concepts tabs are active */ + display: block; + /* Show only when graph or concepts tabs are active */ } .nexus-mobile-tab-content { @@ -542,6 +624,7 @@ main { opacity: 0; transform: translateY(10px); } + to { opacity: 1; transform: translateY(0); @@ -621,9 +704,18 @@ main { } @keyframes quiz-pulse-btn-anim { - 0% { color: rgba(255, 255, 255, 0.5); } - 50% { color: #f43f5e; text-shadow: 0 0 8px rgba(244, 63, 94, 0.6); } - 100% { color: rgba(255, 255, 255, 0.5); } + 0% { + color: rgba(255, 255, 255, 0.5); + } + + 50% { + color: #f43f5e; + text-shadow: 0 0 8px rgba(244, 63, 94, 0.6); + } + + 100% { + color: rgba(255, 255, 255, 0.5); + } } .mobile-insight-body { @@ -648,3 +740,279 @@ main { } /* Obsolescence managed: consolidated mobile toolbar and sheet styled inside respective components */ + +/* Theme-specific Overrides for Light Mode */ +.app-container.theme-light .intelligence-sidebar { + background: #f4f1ea; + border-left: 1px solid rgba(0, 0, 0, 0.08); + box-shadow: -10px 0 30px rgba(139, 130, 115, 0.05); +} + +.app-container.theme-light .resizer { + background: transparent; +} + +.app-container.theme-light .resizer::before { + background: rgba(0, 0, 0, 0.08); +} + +.app-container.theme-light .resizer::after { + background: rgba(0, 0, 0, 0.12); + box-shadow: 0 2px 8px rgba(139, 130, 115, 0.12); +} + +.app-container.theme-light .resizer:hover::before, +.app-container.theme-light.is-resizing .resizer::before { + background: rgba(0, 0, 0, 0.15); +} + +.app-container.theme-light .resizer:hover::after, +.app-container.theme-light.is-resizing .resizer::after { + background: #10b981; + width: 6px; + height: 80px; + box-shadow: 0 0 12px rgba(16, 185, 129, 0.4); +} + +.app-container.theme-light .intelligence-header { + background: rgba(0, 0, 0, 0.02); + border-bottom: 1px solid rgba(0, 0, 0, 0.08); + color: #292524; +} + +.app-container.theme-light .close-btn { + color: #878378; +} + +.app-container.theme-light .close-btn:hover { + color: #292524; +} + +.app-container.theme-light .visual-workspace { + border-bottom: 1px solid rgba(0, 0, 0, 0.08); +} + +.app-container.theme-light .contextual-intelligence-panel { + background: #f4f1ea; + border-top: 1px solid rgba(0, 0, 0, 0.05); +} + +.app-container.theme-light .panel-header { + background: rgba(0, 0, 0, 0.01); + border-bottom: 1px solid rgba(0, 0, 0, 0.05); +} + +.app-container.theme-light .panel-title { + color: #78716c; +} + +.app-container.theme-light .no-node-selected { + color: #878378; +} + +.app-container.theme-light .placeholder-glow { + background: radial-gradient(circle, rgba(16, 185, 129, 0.15) 0%, transparent 70%); +} + +.app-container.theme-light .node-header-section { + border-bottom: 1px solid rgba(0, 0, 0, 0.08); +} + +.app-container.theme-light .node-label { + color: #292524; +} + +.app-container.theme-light .node-details .section-title { + color: #78716c; +} + +.app-container.theme-light .neon-sub-header { + border-left: 2px solid #10b981; + text-shadow: none; +} + +.app-container.theme-light .node-description { + color: #292524; +} + +.app-container.theme-light .node-summary { + color: #44403c; + background: rgba(0, 0, 0, 0.02); + border-left: 2px solid rgba(0, 0, 0, 0.1); +} + +.app-container.theme-light .key-term-item { + color: #292524; +} + +.app-container.theme-light .term-bullet { + color: #10b981; + filter: none; +} + +.app-container.theme-light .sidebar-footer { + background: #f4f1ea; + border-top: 1px solid rgba(0, 0, 0, 0.08); +} + +.app-container.theme-light .open-quiz-btn { + background: rgba(16, 185, 129, 0.03); + border: 1px solid rgba(16, 185, 129, 0.3); + color: #10b981; + box-shadow: 0 4px 15px rgba(16, 185, 129, 0.05); +} + +.app-container.theme-light .open-quiz-btn:hover { + background: rgba(16, 185, 129, 0.08); + border-color: #10b981; + color: #10b981; + box-shadow: 0 0 20px rgba(16, 185, 129, 0.15); +} + +.app-container.theme-light .quiz-pulse-btn { + animation: quiz-pulse-btn-light 2s infinite ease-in-out; +} + +@keyframes quiz-pulse-btn-light { + 0% { + border-color: rgba(16, 185, 129, 0.3); + box-shadow: 0 0 5px rgba(16, 185, 129, 0.05); + } + + 50% { + border-color: #10b981; + box-shadow: 0 0 20px rgba(16, 185, 129, 0.2); + } + + 100% { + border-color: rgba(16, 185, 129, 0.3); + box-shadow: 0 0 5px rgba(16, 185, 129, 0.05); + } +} + +.app-container.theme-light .quiz-nav { + border-bottom: 1px solid rgba(0, 0, 0, 0.08); + background: rgba(0, 0, 0, 0.01); +} + +.app-container.theme-light .back-to-graph-btn { + color: #78716c; +} + +.app-container.theme-light .back-to-graph-btn:hover { + color: #10b981; + background: rgba(0, 0, 0, 0.03); +} + +.app-container.theme-light .mobile-insight-body { + background: #f4f1ea; +} + +.app-container.theme-light .mobile-insight-header { + background: #f4f1ea; + border-bottom: 1px solid rgba(0, 0, 0, 0.08); +} + +.app-container.theme-light .mobile-insight-nav { + background: rgba(0, 0, 0, 0.02); + border: 1px solid rgba(0, 0, 0, 0.05); +} + +.app-container.theme-light .mobile-insight-nav-btn { + color: #78716c; +} + +.app-container.theme-light .mobile-insight-nav-btn.active { + background: rgba(16, 185, 129, 0.1); + color: #10b981; + box-shadow: 0 0 10px rgba(16, 185, 129, 0.1); +} + +.app-container.theme-light .skeleton-line { + background: linear-gradient(90deg, rgba(0, 0, 0, 0.03) 25%, rgba(0, 0, 0, 0.08) 50%, rgba(0, 0, 0, 0.03) 75%); +} + +.app-container.theme-light .clear-summary-btn { + color: rgba(0, 0, 0, 0.4); +} + +.app-container.theme-light .clear-summary-btn:hover { + color: #ef4444; + background: rgba(239, 68, 68, 0.08); +} + +/* Skeleton Loader for Selection Summary */ +.skeleton-container { + display: flex; + flex-direction: column; + gap: 0.75rem; + padding: 0.5rem 0; +} + +.skeleton-line { + height: 0.75rem; + background: linear-gradient(90deg, rgba(255, 255, 255, 0.03) 25%, rgba(255, 255, 255, 0.08) 50%, rgba(255, 255, 255, 0.03) 75%); + background-size: 200% 100%; + animation: skeleton-shimmer 1.5s infinite linear; + border-radius: 4px; +} + +.skeleton-line.title { + height: 1.25rem; + width: 60%; + margin-bottom: 0.5rem; +} + +.skeleton-line.w-90 { + width: 90%; +} + +.skeleton-line.w-80 { + width: 80%; +} + +.skeleton-line.w-70 { + width: 70%; +} + +.skeleton-line.w-60 { + width: 60%; +} + +@keyframes skeleton-shimmer { + 0% { + background-position: 200% 0; + } + + 100% { + background-position: -200% 0; + } +} + +.summary-badge-row { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; +} + +/* Clear Summary Button styling */ +.clear-summary-btn { + background: transparent; + border: none; + color: rgba(255, 255, 255, 0.4); + font-size: 1.25rem; + line-height: 1; + cursor: pointer; + padding: 0.2rem 0.4rem; + display: flex; + align-items: center; + justify-content: center; + border-radius: 4px; + transition: all 0.2s ease; +} + +.clear-summary-btn:hover { + color: #ef4444; + background: rgba(239, 68, 68, 0.1); +} \ No newline at end of file diff --git a/src/NexusReader.UI.Shared/Models/ReaderModels.cs b/src/NexusReader.UI.Shared/Models/ReaderModels.cs index de527e2..9340821 100644 --- a/src/NexusReader.UI.Shared/Models/ReaderModels.cs +++ b/src/NexusReader.UI.Shared/Models/ReaderModels.cs @@ -15,7 +15,7 @@ public enum MobileReaderTab /// /// Screen coordinates for text selection popup positioning. /// -public record SelectionCoordinates(double Top, double Left, double Width); +public record SelectionCoordinates(double Top, double Left, double Width, double Height, double Bottom, double ViewportWidth); /// /// Represents a message in the KM-RAG global and mobile intelligence chat threads. diff --git a/src/NexusReader.UI.Shared/Services/KnowledgeCoordinator.cs b/src/NexusReader.UI.Shared/Services/KnowledgeCoordinator.cs index cdf2008..5d90270 100644 --- a/src/NexusReader.UI.Shared/Services/KnowledgeCoordinator.cs +++ b/src/NexusReader.UI.Shared/Services/KnowledgeCoordinator.cs @@ -22,12 +22,21 @@ public sealed partial class KnowledgeCoordinator : IDisposable, IAsyncDisposable public string CurrentFullPageContent { get; private set; } = string.Empty; + public bool IsLoadingSelectionSummary { get; private set; } + public string? SelectionSummary { get; private set; } + public string? SelectedTextContext { get; private set; } + /// /// Raised when the knowledge graph has been updated with new data. /// Subscribers must return a Task to enable proper async handling. /// public event Func? OnGraphUpdated; + /// + /// Raised when the selection summary state has changed (loading started, finished, or cleared). + /// + public event Func? OnSelectionSummaryStateChanged; + public KnowledgeCoordinator( IKnowledgeService knowledgeService, IKnowledgeGraphService graphService, @@ -205,6 +214,51 @@ public sealed partial class KnowledgeCoordinator : IDisposable, IAsyncDisposable } } + public async Task StartSelectionSummaryAsync(string text, string tenantId = "global") + { + if (string.IsNullOrWhiteSpace(text)) return; + + IsLoadingSelectionSummary = true; + SelectionSummary = null; + SelectedTextContext = text; + if (OnSelectionSummaryStateChanged != null) + { + await OnSelectionSummaryStateChanged.Invoke(); + } + + try + { + var result = await RequestSummaryAndQuizAsync(text, tenantId); + if (result.IsSuccess) + { + SelectionSummary = result.Value.Summary; + } + else + { + _logger.LogWarning("[KnowledgeCoordinator] Selection summary request failed: {Errors}", string.Join(", ", result.Errors.Select(e => e.Message))); + } + } + finally + { + IsLoadingSelectionSummary = false; + if (OnSelectionSummaryStateChanged != null) + { + await OnSelectionSummaryStateChanged.Invoke(); + } + } + } + + public async Task ClearSelectionSummaryAsync() + { + SelectionSummary = null; + SelectedTextContext = null; + IsLoadingSelectionSummary = false; + if (OnSelectionSummaryStateChanged != null) + { + await OnSelectionSummaryStateChanged.Invoke(); + } + } + public async Task ClearAsync() { CancelAndDisposeCts(ref _graphCts); @@ -213,6 +267,7 @@ public sealed partial class KnowledgeCoordinator : IDisposable, IAsyncDisposable CurrentFullPageContent = string.Empty; await _graphService.Clear(); await _quizService.SetQuiz(null, null); + await ClearSelectionSummaryAsync(); } public void Dispose() diff --git a/src/NexusReader.UI.Shared/wwwroot/app.css b/src/NexusReader.UI.Shared/wwwroot/app.css index 2238d4d..cbe822d 100644 --- a/src/NexusReader.UI.Shared/wwwroot/app.css +++ b/src/NexusReader.UI.Shared/wwwroot/app.css @@ -3,37 +3,83 @@ :root { --nexus-neon: #00ff99; --nexus-neon-glow: rgba(0, 255, 153, 0.3); - --nexus-bg: #121212; - --nexus-card: #1a1a1a; + --nexus-bg: #121214; + --nexus-card: #1a1a1e; --nexus-text: #ffffff; --nexus-paper: #F9F9F9; --nexus-font-sans: 'Inter', sans-serif; --nexus-font-serif: 'Merriweather', serif; - - /* Global Semantic Theme Mapping */ - --nexus-primary: var(--nexus-neon); - --nexus-primary-glow: var(--nexus-neon-glow); - --nexus-primary-hover: #00e688; - - /* Standard Layout Tokens */ - --radius-sm: 8px; - --radius-md: 12px; - --radius-lg: 16px; - --radius-xl: 20px; - - /* Safe Area Insets with fallbacks */ - --safe-area-inset-top: env(safe-area-inset-top, 0px); - --safe-area-inset-bottom: env(safe-area-inset-bottom, 0px); - --safe-area-inset-left: env(safe-area-inset-left, 0px); - --safe-area-inset-right: env(safe-area-inset-right, 0px); - /* Transitions */ - --nexus-transition: 0.4s cubic-bezier(0.4, 0, 0.2, 1); + /* Global Selection Style Override */ + --nexus-selection: rgba(0, 255, 153, 0.25); + + /* Graph Nodes Theme Custom Properties (Dark Mode) */ + --nexus-graph-bg: radial-gradient(circle, #1a1a1a 0%, #121212 100%); + --nexus-graph-link-secondary: rgba(255, 255, 255, 0.2); + --nexus-graph-link-default: rgba(255, 255, 255, 0.1); + + --nexus-node-pill-bg: rgba(20, 20, 20, 0.95); + + --nexus-node-rule: #ff4646; + --nexus-node-rule-bg: rgba(255, 70, 70, 0.1); + --nexus-node-rule-text: #ff8b8b; + + --nexus-node-definition: #ffb03a; + --nexus-node-definition-bg: rgba(255, 176, 58, 0.1); + --nexus-node-definition-text: #ffd18c; + + --nexus-node-table: #d946ef; + --nexus-node-table-bg: rgba(217, 70, 239, 0.1); + --nexus-node-table-text: #f5d0fe; + + --nexus-node-section: #3b82f6; + --nexus-node-section-bg: rgba(59, 130, 246, 0.1); + --nexus-node-section-text: #93c5fd; + + --nexus-node-bridge: #06b6d4; + --nexus-node-bridge-bg: rgba(6, 182, 212, 0.1); + --nexus-node-bridge-text: #67e8f9; + + --nexus-node-current: var(--nexus-neon); + --nexus-node-current-bg: rgba(0, 255, 153, 0.15); + --nexus-node-current-text: #ffffff; + + --nexus-node-concept: #00d2c4; + --nexus-node-concept-bg: rgba(0, 210, 196, 0.05); + --nexus-node-concept-text: #e0e0e0; +} + +::selection { + background-color: var(--nexus-selection); + color: inherit; +} + + +/* Global Semantic Theme Mapping */ +--nexus-primary: var(--nexus-neon); +--nexus-primary-glow: var(--nexus-neon-glow); +--nexus-primary-hover: #00e688; + +/* Standard Layout Tokens */ +--radius-sm: 8px; +--radius-md: 12px; +--radius-lg: 16px; +--radius-xl: 20px; + +/* Safe Area Insets with fallbacks */ +--safe-area-inset-top: env(safe-area-inset-top, 0px); +--safe-area-inset-bottom: env(safe-area-inset-bottom, 0px); +--safe-area-inset-left: env(safe-area-inset-left, 0px); +--safe-area-inset-right: env(safe-area-inset-right, 0px); + +/* Transitions */ +--nexus-transition: 0.4s cubic-bezier(0.4, 0, 0.2, 1); } /* Global Glassmorphism with Fallback */ .glass-panel { - background: rgba(20, 20, 20, 0.85); /* Darker fallback for readability */ + background: rgba(20, 20, 20, 0.85); + /* Darker fallback for readability */ border: 1px solid rgba(255, 255, 255, 0.05); border-radius: var(--radius-xl); padding: 1.5rem; @@ -61,32 +107,157 @@ border: none; text-decoration: none; } + .btn-nexus-primary { background: var(--nexus-neon); color: #000000; } + .btn-nexus-secondary { background: rgba(255, 255, 255, 0.05); border: 1px solid rgba(255, 255, 255, 0.1); color: #ffffff; } + .btn-nexus:hover { transform: translateY(-2px); filter: brightness(1.1); } + .btn-nexus-primary:hover { box-shadow: 0 4px 15px var(--nexus-primary-glow); } + .btn-nexus-secondary:hover { box-shadow: 0 4px 15px rgba(255, 255, 255, 0.05); } .theme-light { - --nexus-bg: var(--nexus-paper); + --nexus-bg: #f4f1ea; --nexus-card: #ffffff; - --nexus-text: #121212; + --nexus-text: #2d2a26; + --nexus-selection: rgba(16, 185, 129, 0.18); + + /* Graph Nodes Theme Custom Properties (Light Mode) */ + --nexus-graph-bg: radial-gradient(circle, #ffffff 0%, #e8e4da 100%); + --nexus-graph-link-secondary: rgba(0, 0, 0, 0.15); + --nexus-graph-link-default: rgba(0, 0, 0, 0.08); + + --nexus-node-pill-bg: #fbfafa; + + --nexus-node-rule: #dc2626; + --nexus-node-rule-bg: rgba(220, 38, 38, 0.05); + --nexus-node-rule-text: #991b1b; + + --nexus-node-definition: #d97706; + --nexus-node-definition-bg: rgba(217, 119, 6, 0.05); + --nexus-node-definition-text: #92400e; + + --nexus-node-table: #c084fc; + --nexus-node-table-bg: rgba(192, 132, 252, 0.05); + --nexus-node-table-text: #6b21a8; + + --nexus-node-section: #2563eb; + --nexus-node-section-bg: rgba(37, 99, 235, 0.05); + --nexus-node-section-text: #1e3a8a; + + --nexus-node-bridge: #0891b2; + --nexus-node-bridge-bg: rgba(8, 145, 178, 0.05); + --nexus-node-bridge-text: #155e75; + + --nexus-node-current: #10b981; + --nexus-node-current-bg: rgba(16, 185, 129, 0.08); + --nexus-node-current-text: #064e3b; + + --nexus-node-concept: #0d9488; + --nexus-node-concept-bg: rgba(13, 148, 136, 0.03); + --nexus-node-concept-text: #115e59; + + --nexus-accent: #10b981; } +.theme-light .knowledge-graph-container svg { + background: radial-gradient(circle, #ffffff 0%, #e8e4da 100%) !important; +} + +.theme-light .graph-controls { + background: rgba(254, 254, 253, 0.4) !important; + border: 1px solid rgba(0, 0, 0, 0.08) !important; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.04) !important; +} + +.theme-light .zoom-btn { + background: rgba(0, 0, 0, 0.02) !important; + border: 1px solid rgba(0, 0, 0, 0.06) !important; + color: #78716c !important; +} + +.theme-light .zoom-btn:hover { + background: rgba(16, 185, 129, 0.05) !important; + color: #10b981 !important; + border-color: #10b981 !important; +} + +.theme-light .loading-state { + color: #292524 !important; + background: rgba(254, 254, 254, 0.85) !important; + border: 1px solid rgba(0, 0, 0, 0.05) !important; + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.05) !important; +} + +.theme-light .neon-pulse { + color: #10b981 !important; + filter: none !important; + animation: robot-pulse-light 2s infinite ease-in-out !important; +} + +.theme-light .scan-line { + background: #10b981 !important; + box-shadow: 0 0 10px rgba(16, 185, 129, 0.5) !important; +} + +.theme-light .nexus-node-active { + stroke: #10b981 !important; + filter: drop-shadow(0 0 8px rgba(16, 185, 129, 0.2)) !important; +} + +@keyframes robot-pulse-light { + 0% { transform: scale(1); } + 50% { transform: scale(1.05); } + 100% { transform: scale(1); } +} + +.theme-light ::-webkit-scrollbar-thumb { + background: rgba(0, 0, 0, 0.1); +} + +.theme-light ::-webkit-scrollbar-thumb:hover { + background: rgba(0, 0, 0, 0.2); +} + +.theme-light .glass-panel { + background: rgba(255, 255, 255, 0.85); + border: 1px solid rgba(0, 0, 0, 0.05); +} + +@supports (backdrop-filter: blur(10px)) { + .theme-light .glass-panel { + background: rgba(255, 255, 255, 0.7); + backdrop-filter: blur(10px); + } +} + +.theme-light .btn-nexus-secondary { + background: rgba(0, 0, 0, 0.04); + border: 1px solid rgba(0, 0, 0, 0.08); + color: #292524; +} + +.theme-light .btn-nexus-secondary:hover { + background: rgba(0, 0, 0, 0.08); + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.05); +} + * { @@ -101,13 +272,13 @@ body { font-family: var(--nexus-font-sans); margin: 0; padding: 0; - + /* Handle Notches */ padding-top: var(--safe-area-inset-top); padding-bottom: var(--safe-area-inset-bottom); padding-left: var(--safe-area-inset-left); padding-right: var(--safe-area-inset-right); - + min-height: 100vh; overflow-x: hidden; } @@ -133,14 +304,14 @@ body { /* Platform Specific Tweaks */ -.platform-mobile .nexus-button { +.platform-mobile .nexus-btn { min-height: var(--touch-target-size); min-width: var(--touch-target-size); font-size: 1.1rem; padding: 12px 24px; } -.platform-desktop .nexus-button { +.platform-desktop .nexus-btn { min-height: 36px; font-size: 0.9rem; padding: 8px 16px; @@ -164,7 +335,8 @@ h1:focus { } /* Preloader Styles */ -#app-preloader, .app-preloader { +#app-preloader, +.app-preloader { position: fixed; top: 0; left: 0; @@ -206,12 +378,25 @@ h1:focus { } @keyframes spin { - 0% { transform: rotate(0deg); } - 100% { transform: rotate(360deg); } + 0% { + transform: rotate(0deg); + } + + 100% { + transform: rotate(360deg); + } } @keyframes pulse { - 0%, 100% { opacity: 1; transform: scale(1); } - 50% { opacity: 0.5; transform: scale(0.95); } -} - \ No newline at end of file + + 0%, + 100% { + opacity: 1; + transform: scale(1); + } + + 50% { + opacity: 0.5; + transform: scale(0.95); + } +} \ No newline at end of file diff --git a/src/NexusReader.UI.Shared/wwwroot/js/knowledgeGraph.js b/src/NexusReader.UI.Shared/wwwroot/js/knowledgeGraph.js index d5f671f..1e023b9 100644 --- a/src/NexusReader.UI.Shared/wwwroot/js/knowledgeGraph.js +++ b/src/NexusReader.UI.Shared/wwwroot/js/knowledgeGraph.js @@ -40,70 +40,70 @@ const getCategoryStyle = d => { // 1. Rule (red/coral) if (type === 'rule') { return { - color: '#ff4646', - fill: 'rgba(255, 70, 70, 0.1)', + color: 'var(--nexus-node-rule, #ff4646)', + fill: 'var(--nexus-node-rule-bg, rgba(255, 70, 70, 0.1))', opacity: 0.8, glowKey: 'rule', - textColor: '#ff8b8b' + textColor: 'var(--nexus-node-rule-text, #ff8b8b)' }; } // 2. Definition (gold/amber) if (type === 'definition') { return { - color: '#ffb03a', - fill: 'rgba(255, 176, 58, 0.1)', + color: 'var(--nexus-node-definition, #ffb03a)', + fill: 'var(--nexus-node-definition-bg, rgba(255, 176, 58, 0.1))', opacity: 0.8, glowKey: 'definition', - textColor: '#ffd18c' + textColor: 'var(--nexus-node-definition-text, #ffd18c)' }; } // 3. Table (purple/magenta) if (type === 'table') { return { - color: '#d946ef', - fill: 'rgba(217, 70, 239, 0.1)', + color: 'var(--nexus-node-table, #d946ef)', + fill: 'var(--nexus-node-table-bg, rgba(217, 70, 239, 0.1))', opacity: 0.8, glowKey: 'table', - textColor: '#f5d0fe' + textColor: 'var(--nexus-node-table-text, #f5d0fe)' }; } // 4. Section (blue/indigo) if (type === 'section') { return { - color: '#3b82f6', - fill: 'rgba(59, 130, 246, 0.1)', + color: 'var(--nexus-node-section, #3b82f6)', + fill: 'var(--nexus-node-section-bg, rgba(59, 130, 246, 0.1))', opacity: 0.8, glowKey: 'section', - textColor: '#93c5fd' + textColor: 'var(--nexus-node-section-text, #93c5fd)' }; } // 5. Bridge (cyan/comparison) if (group === 'bridge') { return { - color: '#06b6d4', - fill: 'rgba(6, 182, 212, 0.1)', + color: 'var(--nexus-node-bridge, #06b6d4)', + fill: 'var(--nexus-node-bridge-bg, rgba(6, 182, 212, 0.1))', opacity: 0.7, glowKey: 'bridge', - textColor: '#67e8f9' + textColor: 'var(--nexus-node-bridge-text, #67e8f9)' }; } // 6. Current (active/focus landmark - neon green) if (group === 'current') { return { - color: 'var(--nexus-neon)', - fill: 'rgba(0, 255, 153, 0.15)', + color: 'var(--nexus-node-current, var(--nexus-neon))', + fill: 'var(--nexus-node-current-bg, rgba(0, 255, 153, 0.15))', opacity: 0.9, glowKey: 'current', - textColor: '#ffffff' + textColor: 'var(--nexus-node-current-text, #ffffff)' }; } // 7. Concept / Default (subtle cool steel blue/teal) return { - color: '#00d2c4', - fill: 'rgba(0, 210, 196, 0.05)', + color: 'var(--nexus-node-concept, #00d2c4)', + fill: 'var(--nexus-node-concept-bg, rgba(0, 210, 196, 0.05))', opacity: 0.4, glowKey: 'concept', - textColor: '#e0e0e0' + textColor: 'var(--nexus-node-concept-text, #e0e0e0)' }; }; @@ -131,16 +131,16 @@ const getNodeGlyph = d => { function updateNodeAppearances() { if (!node) return; - - node.each(function(d) { + + node.each(function (d) { const g = d3.select(this); const rect = g.select(".node-pill"); const text = g.select("text"); - + const isCurrent = getNodeGroup(d) === 'current'; const isSelected = activeNodeId && d.id === activeNodeId; const showFull = !isMobileMode || isSelected || isCurrent; - + if (showFull) { rect.transition().duration(250) .attr("x", -getPillWidth(d) / 2) @@ -148,7 +148,7 @@ function updateNodeAppearances() { .attr("height", 30) .attr("rx", 15) .attr("y", -15); - + text.text(getDisplayLabel(d)) .attr("font-size", isCurrent || isSelected ? "0.85rem" : "0.8rem") .attr("font-weight", isCurrent || isSelected ? "600" : "normal"); @@ -159,7 +159,7 @@ function updateNodeAppearances() { .attr("height", 30) .attr("rx", 15) .attr("y", -15); - + text.text(getNodeGlyph(d)) .attr("font-size", "0.9rem") .attr("font-weight", "bold"); @@ -170,7 +170,7 @@ function updateNodeAppearances() { export function setMobileMode(isMobile) { isMobileMode = isMobile; if (!simulation) return; - + if (isMobile) { simulation.force("charge", d3.forceManyBody().strength(-60)); simulation.force("link").distance(180); @@ -187,7 +187,7 @@ export function setMobileMode(isMobile) { simulation.force("link").distance(120); simulation.force("collide", d3.forceCollide().radius(d => (getPillWidth(d) / 2) + 20)); } - + updateNodeAppearances(); simulation.alpha(0.3).restart(); } @@ -208,11 +208,11 @@ export function mount(containerId, data, dotNetHelper) { .attr("viewBox", [0, 0, width, height]) .attr("width", "100%") .attr("height", "100%") - .style("background", "radial-gradient(circle, #1a1a1a 0%, #121212 100%)"); + .style("background", "var(--nexus-graph-bg, radial-gradient(circle, #1a1a1a 0%, #121212 100%))"); // Radial gradients for Nebula effects const defs = svgElement.append("defs"); - + // Fallback radial gradient for legacy nebulaGlow const radialGradient = defs.append("radialGradient") .attr("id", "nebulaGlow") @@ -223,13 +223,13 @@ export function mount(containerId, data, dotNetHelper) { radialGradient.append("stop").attr("offset", "100%").attr("stop-color", "var(--nexus-neon)").attr("stop-opacity", 0); const colors = { - 'rule': '#ff4646', - 'definition': '#ffb03a', - 'table': '#d946ef', - 'section': '#3b82f6', - 'bridge': '#06b6d4', - 'current': 'var(--nexus-neon)', - 'concept': '#00d2c4' + 'rule': 'var(--nexus-node-rule, #ff4646)', + 'definition': 'var(--nexus-node-definition, #ffb03a)', + 'table': 'var(--nexus-node-table, #d946ef)', + 'section': 'var(--nexus-node-section, #3b82f6)', + 'bridge': 'var(--nexus-node-bridge, #06b6d4)', + 'current': 'var(--nexus-node-current, var(--nexus-neon))', + 'concept': 'var(--nexus-node-concept, #00d2c4)' }; Object.entries(colors).forEach(([key, color]) => { @@ -275,7 +275,7 @@ export function mount(containerId, data, dotNetHelper) { zoomBehavior = d3.zoom() .scaleExtent([0.3, 4]) .on("zoom", (e) => rootGroup.attr("transform", e.transform)); - + svgElement.call(zoomBehavior).on("wheel.zoom", null); // Use ResizeObserver for more reliable container size tracking @@ -324,7 +324,7 @@ export function mount(containerId, data, dotNetHelper) { return `translate(${d.x},${d.y})`; }); } - + if (badge && badge.style("display") !== "none") { const activeData = badge.datum(); if (activeData) { @@ -377,9 +377,9 @@ export function updateData(data) { enter => enter.append("path") .attr("stroke", d => { if (d.type === 'Defines' || d.type === 'maps_to') return 'var(--nexus-accent, #00ffaa)'; - if (d.type === 'Next' || d.type === 'relates_to') return 'rgba(255,255,255,0.2)'; + if (d.type === 'Next' || d.type === 'relates_to') return 'var(--nexus-graph-link-secondary, rgba(255,255,255,0.2))'; if (d.type === 'Contains' || d.type === 'contains') return 'var(--nexus-neon)'; - return 'rgba(255,255,255,0.1)'; + return 'var(--nexus-graph-link-default, rgba(255,255,255,0.1))'; }) .attr("fill", "none") .attr("stroke-width", d => (d.type === 'Defines' || d.type === 'maps_to') ? 2 : 1) @@ -413,7 +413,7 @@ export function updateData(data) { g.append("rect") .attr("class", "node-pill") - .attr("fill", "rgba(20, 20, 20, 0.95)") + .attr("fill", "var(--nexus-node-pill-bg, rgba(20, 20, 20, 0.95))") .attr("stroke", d => getCategoryStyle(d).color) .attr("stroke-width", d => getNodeGroup(d) === 'current' ? 2 : 1.2); @@ -424,9 +424,9 @@ export function updateData(data) { g.append("title") .text(d => d.description ? `${d.label}\n\n${d.description}` : d.label); - + g.transition().duration(500).style("opacity", 1); - + return g; }, update => update.classed("neon-flash-node", false), @@ -466,7 +466,7 @@ function drag(simulation) { export function setActiveNode(nodeId) { if (!svgElement || !node) return; - + activeNodeId = nodeId; // Safety check: ensure we only target the first occurrence if IDs are duplicated const targetNode = node.filter(d => d.id === nodeId); @@ -479,7 +479,7 @@ export function setActiveNode(nodeId) { const firstMatch = targetNode.filter((d, i) => i === 0); const d = firstMatch.datum(); if (!d || d.x === undefined || d.y === undefined || isNaN(d.x) || !isFinite(d.x) || isNaN(d.y) || !isFinite(d.y)) return; - + // Reset all active classes rootGroup.selectAll(".node-pill").classed("nexus-node-active", false); firstMatch.select(".node-pill").classed("nexus-node-active", true); @@ -502,7 +502,7 @@ export function setActiveNode(nodeId) { return 20; })); } - + updateNodeAppearances(); // Smooth transition to the first matching node @@ -514,10 +514,10 @@ 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 => { @@ -558,7 +558,7 @@ export function handleResize(containerId) { svgElement.attr("viewBox", [0, 0, width, height]); simulation.force("center", d3.forceCenter(width / 2, height / 2)); - + const prevMobileMode = isMobileMode; isMobileMode = window.innerWidth < 768; if (isMobileMode !== prevMobileMode) { diff --git a/src/NexusReader.UI.Shared/wwwroot/js/selectionHandler.js b/src/NexusReader.UI.Shared/wwwroot/js/selectionHandler.js index f054250..0c990fe 100644 --- a/src/NexusReader.UI.Shared/wwwroot/js/selectionHandler.js +++ b/src/NexusReader.UI.Shared/wwwroot/js/selectionHandler.js @@ -1,7 +1,70 @@ +export function positionToolbar() { + const toolbarElement = document.querySelector('.selection-ai-panel'); + if (!toolbarElement) return; + + const selection = window.getSelection(); + if (selection.isCollapsed) return; + + const range = selection.getRangeAt(0); + const rects = range.getClientRects(); + if (!rects || rects.length === 0) return; + + const firstRect = rects[0]; + const combinedRect = range.getBoundingClientRect(); + + // Find the canvas container (which is the positioned parent) + const canvasElement = document.querySelector('.reader-canvas'); + let canvasRect = { top: 0, left: 0 }; + let scrollTop = 0; + let scrollLeft = 0; + + if (canvasElement) { + canvasRect = canvasElement.getBoundingClientRect(); + scrollTop = canvasElement.scrollTop; + scrollLeft = canvasElement.scrollLeft; + } + + const toolbarWidth = toolbarElement.offsetWidth; + const toolbarHeight = toolbarElement.offsetHeight; + + // Oblicz środek zaznaczenia w poziomie + const left = (combinedRect.left - canvasRect.left) + scrollLeft + (combinedRect.width / 2) - (toolbarWidth / 2); + + // Warunek brzegowy (Top Screen Fallback) + const relativeTop = firstRect.top - toolbarHeight - 14; + + let top; + let below = false; + if (relativeTop < 0) { + // Pozwól wskoczyć POD zaznaczony tekst + top = (combinedRect.bottom - canvasRect.top) + scrollTop + 12; + below = true; + toolbarElement.classList.add('below'); + } else { + top = (firstRect.top - canvasRect.top) + scrollTop - toolbarHeight - 14; + toolbarElement.classList.remove('below'); + } + + toolbarElement.style.left = `${left}px`; + toolbarElement.style.top = `${top}px`; + + return { + left: left, + top: top, + below: below + }; +} +let currentHandleSelection = null; +let currentMouseUpHandler = null; +let currentContainer = null; + export function initSelectionListener(dotNetHelper, container) { if (!container) return; console.log("[SelectionHandler] Initializing..."); + + // Clean up any existing listeners first + destroySelectionListener(); const handleSelection = () => { const selection = window.getSelection(); @@ -16,26 +79,60 @@ export function initSelectionListener(dotNetHelper, container) { const blockNode = node.closest('[id]'); - if (blockNode) { - const rect = range.getBoundingClientRect(); + if (blockNode) { + const rects = range.getClientRects(); + const firstRect = rects && rects.length > 0 ? rects[0] : null; + const lastRect = rects && rects.length > 0 ? rects[rects.length - 1] : null; + const combinedRect = range.getBoundingClientRect(); - console.log("[SelectionHandler] Selection at screen coords:", rect.top, rect.left); + const topVal = firstRect ? firstRect.top : combinedRect.top; + const bottomVal = lastRect ? lastRect.bottom : combinedRect.bottom; - dotNetHelper.invokeMethodAsync('HandleTextSelected', - text, - blockNode.id, - { - Top: rect.top, - Left: rect.left, - Width: rect.width - }); - } + console.log("[SelectionHandler] Selection coords (first/last/combined):", topVal, bottomVal, combinedRect.left); + + dotNetHelper.invokeMethodAsync('HandleTextSelected', + text, + blockNode.id, + { + Top: topVal, + Left: combinedRect.left, + Width: combinedRect.width, + Height: combinedRect.height, + Bottom: bottomVal, + ViewportWidth: window.innerWidth + }); + + // Reposition the toolbar if already present + setTimeout(positionToolbar, 0); + } } else { dotNetHelper.invokeMethodAsync('HandleSelectionCleared'); } }; - // Use multiple triggers for maximum reliability - document.addEventListener('selectionchange', handleSelection); - container.addEventListener('mouseup', () => setTimeout(handleSelection, 10)); + const mouseUpHandler = () => setTimeout(handleSelection, 10); + + currentHandleSelection = handleSelection; + currentMouseUpHandler = mouseUpHandler; + currentContainer = container; + + document.addEventListener('selectionchange', currentHandleSelection); + currentContainer.addEventListener('mouseup', currentMouseUpHandler); } + +export function destroySelectionListener() { + if (currentHandleSelection) { + document.removeEventListener('selectionchange', currentHandleSelection); + currentHandleSelection = null; + } + if (currentMouseUpHandler && currentContainer) { + currentContainer.removeEventListener('mouseup', currentMouseUpHandler); + currentMouseUpHandler = null; + currentContainer = null; + } +} + +export function getSelectionText() { + return window.getSelection().toString(); +} + diff --git a/src/NexusReader.Web/Program.cs b/src/NexusReader.Web/Program.cs index 38c8006..66ef13b 100644 --- a/src/NexusReader.Web/Program.cs +++ b/src/NexusReader.Web/Program.cs @@ -204,7 +204,8 @@ using (var scope = app.Services.CreateScope()) logger.LogInformation("Próba połączenia z bazą danych (próba {Attempt}/{MaxRetries})...", i + 1, maxRetries); } - await dbContext.Database.MigrateAsync(); + + await DbInitializer.SeedAsync(services); await TriggerBackgroundProcessingForUnindexedBooksAsync(services);