style(ui): refactor reader layout grid, fix focus mode layout collapse, fix SVG rendering dots, reorganize intelligence toolbar #69

Merged
mjasin merged 10 commits from feature/reader-visual-refactor into develop 2026-06-05 09:51:29 +00:00
5 changed files with 230 additions and 17 deletions
Showing only changes of commit 89fa5cac19 - Show all commits
@@ -106,14 +106,21 @@
StateHasChanged();
try
{
var module = await JS.InvokeAsync<IJSObjectReference>("import", "./_content/NexusReader.UI.Shared/js/selectionHandler.js");
var selectedText = await module.InvokeAsync<string>("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)
{
_ = Coordinator.StartSelectionSummaryAsync($"{contextPrompt}{selectedText}");
await CloseAsync();
await InteractionService.RequestAssistant();
}
@@ -136,18 +143,28 @@
StateHasChanged();
try
{
var module = await JS.InvokeAsync<IJSObjectReference>("import", "./_content/NexusReader.UI.Shared/js/selectionHandler.js");
var selectedText = await module.InvokeAsync<string>("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}");
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}");
@@ -17,6 +17,7 @@
@inject NavigationManager NavigationManager
@inject Microsoft.Extensions.Logging.ILogger<ReaderLayout> Logger
@inject IThemeService ThemeService
@inject KnowledgeCoordinator Coordinator
@implements IAsyncDisposable
@@ -63,7 +64,32 @@
<span class="panel-title">Contextual Intelligence Panel</span>
</div>
<div class="panel-body">
@if (_selectedNode != null)
@if (Coordinator.IsLoadingSelectionSummary)
{
<div class="skeleton-container">
<div class="skeleton-line title"></div>
<div class="skeleton-line w-90"></div>
<div class="skeleton-line w-80"></div>
<div class="skeleton-line w-70"></div>
<div class="skeleton-line w-60"></div>
</div>
}
else if (!string.IsNullOrEmpty(Coordinator.SelectionSummary))
{
<div class="node-details">
<div class="node-header-section">
<div class="summary-badge-row">
<span class="node-group-badge current">PODSUMOWANIE</span>
<button class="clear-summary-btn" @onclick="ClearSelectionSummary" title="Wyczyść podsumowanie">×</button>
</div>
<h3 class="node-label">Zaznaczony Fragment</h3>
</div>
<div class="detail-section summary-section">
<p class="node-summary">@Coordinator.SelectionSummary</p>
</div>
</div>
}
else if (_selectedNode != null)
{
<div class="node-details">
<div class="node-header-section">
@@ -166,7 +192,32 @@
{
<div class="contextual-intelligence-panel">
<div class="panel-body">
@if (_selectedNode != null)
@if (Coordinator.IsLoadingSelectionSummary)
{
<div class="skeleton-container">
<div class="skeleton-line title"></div>
<div class="skeleton-line w-90"></div>
<div class="skeleton-line w-80"></div>
<div class="skeleton-line w-70"></div>
<div class="skeleton-line w-60"></div>
</div>
}
else if (!string.IsNullOrEmpty(Coordinator.SelectionSummary))
{
<div class="node-details">
<div class="node-header-section">
<div class="summary-badge-row">
<span class="node-group-badge current">PODSUMOWANIE</span>
<button class="clear-summary-btn" @onclick="ClearSelectionSummary" title="Wyczyść podsumowanie">×</button>
</div>
<h3 class="node-label">Zaznaczony Fragment</h3>
</div>
<div class="detail-section summary-section">
<p class="node-summary">@Coordinator.SelectionSummary</p>
</div>
</div>
}
else if (_selectedNode != null)
{
<div class="node-details">
<div class="node-header-section">
@@ -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,13 +410,25 @@
{
if (_isMobile)
{
if (Coordinator.IsLoadingSelectionSummary || !string.IsNullOrEmpty(Coordinator.SelectionSummary))
{
_activeMobileTab = MobileReaderTab.Concepts;
_activeTab = SidebarTab.Knowledge;
}
OpenAssistant();
}
else
{
_activeMobileTab = MobileReaderTab.Concepts;
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
{
@@ -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);
}
@@ -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; }
/// <summary>
/// Raised when the knowledge graph has been updated with new data.
/// Subscribers must return a Task to enable proper async handling.
/// </summary>
public event Func<GraphDataDto, Task>? OnGraphUpdated;
/// <summary>
/// Raised when the selection summary state has changed (loading started, finished, or cleared).
/// </summary>
public event Func<Task>? 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)
mjasin marked this conversation as resolved
Review

🟡 Design/Architecture: If RequestSummaryAndQuizAsync fails, the background task completes, but the loading state is cleared and no error feedback is provided to the UI or logged.

Consider exposing an error state or logging the failure to improve user feedback.

Actionable suggestion:

            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)));
                // optionally set an error property to show in UI
            }
🟡 Design/Architecture: If `RequestSummaryAndQuizAsync` fails, the background task completes, but the loading state is cleared and no error feedback is provided to the UI or logged. Consider exposing an error state or logging the failure to improve user feedback. Actionable suggestion: ```csharp 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))); // optionally set an error property to show in UI } ```
{
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()
@@ -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) {
1
@@ -101,3 +109,7 @@ export function initSelectionListener(dotNetHelper, container) {
container.addEventListener('mouseup', () => setTimeout(handleSelection, 10));
}
export function getSelectionText() {
return window.getSelection().toString();
}