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 {