From fdbe901a804705a4fde4d1c7680ed049b040bc8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Jasi=C5=84ski?= Date: Tue, 2 Jun 2026 20:18:28 +0200 Subject: [PATCH] refactor: redesign selection AI panel to a compact toolbar with independent summary and quiz actions and improved coordinate calculation --- .../Molecules/SelectionAiPanel.razor | 155 ++++++++------ .../Molecules/SelectionAiPanel.razor.css | 201 +++++++----------- .../Models/ReaderModels.cs | 2 +- .../wwwroot/js/selectionHandler.js | 21 +- 4 files changed, 186 insertions(+), 193 deletions(-) diff --git a/src/NexusReader.UI.Shared/Components/Molecules/SelectionAiPanel.razor b/src/NexusReader.UI.Shared/Components/Molecules/SelectionAiPanel.razor index 341fc1b..f570eee 100644 --- a/src/NexusReader.UI.Shared/Components/Molecules/SelectionAiPanel.razor +++ b/src/NexusReader.UI.Shared/Components/Molecules/SelectionAiPanel.razor @@ -3,49 +3,40 @@ @using NexusReader.Application.DTOs.AI @inject KnowledgeCoordinator Coordinator @inject IReaderInteractionService InteractionService +@inject IQuizStateService QuizService @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 +47,89 @@ [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 IsLoadingSummary = false; + private bool IsLoadingQuiz = false; + private bool IsAnyLoading => IsLoadingSummary || IsLoadingQuiz; private bool PositionBelow => Coordinates != null && Coordinates.Top < 250; 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; + // Reset loading states when parameters change + IsLoadingSummary = false; + IsLoadingQuiz = false; } private string PanelStyle => Coordinates != null ? string.Create(System.Globalization.CultureInfo.InvariantCulture, - $"top: {(PositionBelow ? Coordinates.Bottom + 8 : Coordinates.Top - 8):F1}px !important; " + - $"left: {Math.Clamp(Coordinates.Left + Coordinates.Width / 2, 280, 1600):F1}px !important; " + + $"top: {(PositionBelow ? Coordinates.Bottom + 16 : Coordinates.Top - 16):F1}px !important; " + + $"left: {Math.Max(140.0, Math.Min(Coordinates.ViewportWidth - 140.0, Coordinates.Left + Coordinates.Width / 2.0)):F1}px !important; " + $"transform: translate(-50%, {(PositionBelow ? "0" : "-100%")}) !important;") : ""; - private async Task RequestSummary() + private async Task RequestSummaryAsync() { - IsLoading = true; - 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}"); - Packet = result.IsSuccess ? result.Value : null; - IsLoading = false; + if (IsAnyLoading) return; + IsLoadingSummary = true; + StateHasChanged(); + + 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) + { + await CloseAsync(); + await InteractionService.RequestAssistant(); + } + } + catch (Exception ex) + { + Console.WriteLine($"[SelectionAiPanel] Error requesting summary: {ex.Message}"); + } + finally + { + IsLoadingSummary = false; + StateHasChanged(); + } } - private async Task GenerateFullQuiz() + private async Task GenerateQuizAsync() { - IsLoading = true; - await Coordinator.RequestSummaryAndQuizAsync(FullPageContent); - IsLoading = false; - await CloseAsync(); + if (IsAnyLoading) return; + IsLoadingQuiz = true; + StateHasChanged(); + + 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) + { + 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!); } } + diff --git a/src/NexusReader.UI.Shared/Components/Molecules/SelectionAiPanel.razor.css b/src/NexusReader.UI.Shared/Components/Molecules/SelectionAiPanel.razor.css index f99e021..1d3f8b8 100644 --- a/src/NexusReader.UI.Shared/Components/Molecules/SelectionAiPanel.razor.css +++ b/src/NexusReader.UI.Shared/Components/Molecules/SelectionAiPanel.razor.css @@ -1,158 +1,111 @@ .selection-ai-panel { position: fixed; z-index: 9999; - width: 550px; - max-width: 90vw; - animation: fadeInScale 0.2s ease-out; + 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: auto; + 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: translate(-50%, -80%) scale(0.96); + } + to { + opacity: 1; + transform: translate(-50%, -100%) 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: translate(-50%, -20%) scale(0.96); + } + to { + opacity: 1; + transform: translate(-50%, 0) 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; } -} diff --git a/src/NexusReader.UI.Shared/Models/ReaderModels.cs b/src/NexusReader.UI.Shared/Models/ReaderModels.cs index 2f369ef..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, double Height, double Bottom); +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/wwwroot/js/selectionHandler.js b/src/NexusReader.UI.Shared/wwwroot/js/selectionHandler.js index 80ad56b..ff7caa2 100644 --- a/src/NexusReader.UI.Shared/wwwroot/js/selectionHandler.js +++ b/src/NexusReader.UI.Shared/wwwroot/js/selectionHandler.js @@ -17,19 +17,26 @@ export function initSelectionListener(dotNetHelper, container) { const blockNode = node.closest('[id]'); if (blockNode) { - const rect = range.getBoundingClientRect(); + 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; + + console.log("[SelectionHandler] Selection coords (first/last/combined):", topVal, bottomVal, combinedRect.left); dotNetHelper.invokeMethodAsync('HandleTextSelected', text, blockNode.id, { - Top: rect.top, - Left: rect.left, - Width: rect.width, - Height: rect.height, - Bottom: rect.bottom + Top: topVal, + Left: combinedRect.left, + Width: combinedRect.width, + Height: combinedRect.height, + Bottom: bottomVal, + ViewportWidth: window.innerWidth }); } } else {