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);