style(ui): refactor reader layout grid, fix focus mode layout collapse, fix SVG rendering dots, reorganize intelligence toolbar #69
@@ -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<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"
|
||||
: "";
|
||||
|
||||
_ = 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<IJSObjectReference>("import", "./_content/NexusReader.UI.Shared/js/selectionHandler.js");
|
||||
var selectedText = await module.InvokeAsync<string>("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)
|
||||
|
||||
@@ -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,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
|
||||
{
|
||||
|
||||
@@ -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
|
||||
{
|
||||
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) {
|
||||
@@ -101,3 +109,7 @@ export function initSelectionListener(dotNetHelper, container) {
|
||||
container.addEventListener('mouseup', () => setTimeout(handleSelection, 10));
|
||||
}
|
||||
|
||||
export function getSelectionText() {
|
||||
return window.getSelection().toString();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user
🟡 Design/Architecture: If
RequestSummaryAndQuizAsyncfails, 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: