refactor: redesign selection AI panel to a compact toolbar with independent summary and quiz actions and improved coordinate calculation

This commit is contained in:
2026-06-02 20:18:28 +02:00
parent 900690875f
commit fdbe901a80
4 changed files with 186 additions and 193 deletions
@@ -3,49 +3,40 @@
@using NexusReader.Application.DTOs.AI
@inject KnowledgeCoordinator Coordinator
@inject IReaderInteractionService InteractionService
@inject IQuizStateService QuizService
@if (IsVisible)
{
<div class="selection-ai-panel expanded @(PositionBelow ? "below" : "")" style="@PanelStyle">
<div class="ai-bubble">
<div class="ai-avatar">
<div class="avatar-ring"></div>
<NexusIcon Name="robot" Size="48" Class="neon-pulse" />
<div class="avatar-label">
<span class="name">E-Czytnik</span>
<span class="role">Asystent AI</span>
</div>
</div>
<div class="ai-content">
@if (IsLoading)
{
<div class="loading-state">
<div class="shimmer">Skanowanie fragmentu...</div>
</div>
}
else if (Packet != null)
{
<div class="summary-box">
<NexusTypography Variant="NexusTypography.TypographyVariant.UI">@Packet.Summary</NexusTypography>
</div>
<div class="ai-actions">
<button class="action-btn neon-border" @onclick="GenerateFullQuiz">Generuj Quiz dla całej strony</button>
<button class="action-btn ghost" @onclick="CloseAsync">Zamknij</button>
</div>
}
else
{
<div class="summary-box">
<NexusTypography Variant="NexusTypography.TypographyVariant.UI">Wykryto ciekawy fragment! Czy chcesz, abym wygenerował podsumowanie lub quiz z tego rozdziału?</NexusTypography>
</div>
<div class="ai-actions">
<button class="action-btn neon-border" @onclick="RequestSummary">Podsumuj zaznaczenie</button>
<button class="action-btn ghost" @onclick="CloseAsync">Pomiń</button>
</div>
}
</div>
<div class="bubble-pointer"></div>
</div>
<div class="selection-ai-panel @(PositionBelow ? "below" : "")" style="@PanelStyle">
<button class="toolbar-btn primary @(IsLoadingSummary ? "loading" : "") @(IsAnyLoading ? "disabled" : "")"
disabled="@IsAnyLoading"
@onclick="RequestSummaryAsync">
@if (IsLoadingSummary)
{
<span class="spinner-inline"></span>
<span class="btn-text">Podsumowywanie...</span>
}
else
{
<NexusIcon Name="book-open" Size="14" Class="btn-icon" />
<span class="btn-text">Podsumuj</span>
}
</button>
<div class="toolbar-divider"></div>
<button class="toolbar-btn secondary @(IsLoadingQuiz ? "loading" : "") @(IsAnyLoading ? "disabled" : "")"
disabled="@IsAnyLoading"
@onclick="GenerateQuizAsync">
@if (IsLoadingQuiz)
{
<span class="spinner-inline"></span>
<span class="btn-text">Generowanie...</span>
}
else
{
<NexusIcon Name="target" Size="14" Class="btn-icon" />
<span class="btn-text">Quiz</span>
}
</button>
</div>
}
@@ -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!);
}
}