diff --git a/src/NexusReader.UI.Shared/Components/Molecules/SelectionAiPanel.razor b/src/NexusReader.UI.Shared/Components/Molecules/SelectionAiPanel.razor index ba2101c..db859d3 100644 --- a/src/NexusReader.UI.Shared/Components/Molecules/SelectionAiPanel.razor +++ b/src/NexusReader.UI.Shared/Components/Molecules/SelectionAiPanel.razor @@ -107,13 +107,20 @@ try { - 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) + 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(); } @@ -137,15 +144,25 @@ try { - 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) + var module = await JS.InvokeAsync("import", "./_content/NexusReader.UI.Shared/js/selectionHandler.js"); + var selectedText = await module.InvokeAsync("getSelectionText"); + if (string.IsNullOrWhiteSpace(selectedText)) { - await CloseAsync(); - await QuizService.RequestQuiz(BlockId); + 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) diff --git a/src/NexusReader.UI.Shared/Layout/ReaderLayout.razor b/src/NexusReader.UI.Shared/Layout/ReaderLayout.razor index ef16616..97633c3 100644 --- a/src/NexusReader.UI.Shared/Layout/ReaderLayout.razor +++ b/src/NexusReader.UI.Shared/Layout/ReaderLayout.razor @@ -17,6 +17,7 @@ @inject NavigationManager NavigationManager @inject Microsoft.Extensions.Logging.ILogger Logger @inject IThemeService ThemeService +@inject KnowledgeCoordinator Coordinator @implements IAsyncDisposable @@ -63,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) {
@@ -166,7 +192,32 @@ {
- @if (_selectedNode != null) + @if (Coordinator.IsLoadingSelectionSummary) + { +
+
+
+
+
+
+
+ } + else if (!string.IsNullOrEmpty(Coordinator.SelectionSummary)) + { +
+
+
+ PODSUMOWANIE + +
+

Zaznaczony Fragment

+
+
+

@Coordinator.SelectionSummary

+
+
+ } + else if (_selectedNode != null) {
@@ -293,6 +344,7 @@ InteractionService.OnScrollPercentChanged += HandleScrollPercentChanged; GraphService.OnGraphUpdated += HandleGraphUpdatedAsync; ThemeService.OnThemeChanged += HandleThemeChangedAsync; + Coordinator.OnSelectionSummaryStateChanged += HandleUpdate; var context = PlatformService.GetDeviceContext(); if (context.IsSuccess) @@ -333,6 +385,11 @@ StateHasChanged(); } + private async Task ClearSelectionSummary() + { + await Coordinator.ClearSelectionSummaryAsync(); + } + private async Task HandleScrollPercentChanged(int percent) { _scrollPercentage = percent; @@ -353,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); } @@ -450,6 +519,7 @@ 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 cf847ae..64779c3 100644 --- a/src/NexusReader.UI.Shared/Layout/ReaderLayout.razor.css +++ b/src/NexusReader.UI.Shared/Layout/ReaderLayout.razor.css @@ -705,3 +705,66 @@ main { background: #f9f9f9; } +/* 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); +} diff --git a/src/NexusReader.UI.Shared/Services/KnowledgeCoordinator.cs b/src/NexusReader.UI.Shared/Services/KnowledgeCoordinator.cs index cdf2008..5caf2f8 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,47 @@ 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; + } + } + 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 +263,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/js/selectionHandler.js b/src/NexusReader.UI.Shared/wwwroot/js/selectionHandler.js index e216309..ec78f9c 100644 --- a/src/NexusReader.UI.Shared/wwwroot/js/selectionHandler.js +++ b/src/NexusReader.UI.Shared/wwwroot/js/selectionHandler.js @@ -34,9 +34,11 @@ export function positionToolbar() { 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; @@ -45,6 +47,12 @@ export function positionToolbar() { toolbarElement.style.left = `${left}px`; toolbarElement.style.top = `${top}px`; + + return { + left: left, + top: top, + below: below + }; } export function initSelectionListener(dotNetHelper, container) { @@ -101,3 +109,7 @@ export function initSelectionListener(dotNetHelper, container) { container.addEventListener('mouseup', () => setTimeout(handleSelection, 10)); } +export function getSelectionText() { + return window.getSelection().toString(); +} +