style(ui): refactor reader layout grid, fix focus mode layout collapse, fix SVG rendering dots, reorganize intelligence toolbar #69
@@ -106,14 +106,21 @@
|
|||||||
StateHasChanged();
|
StateHasChanged();
|
||||||
|
|
||||||
try
|
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)
|
var contextPrompt = !string.IsNullOrWhiteSpace(FullPageContent)
|
||||||
? $"ANALYSIS CONTEXT (Full Page Content):\n{FullPageContent}\n\nUSER SELECTION TO SUMMARIZE:\n"
|
? $"ANALYSIS CONTEXT (Full Page Content):\n{FullPageContent}\n\nUSER SELECTION TO SUMMARIZE:\n"
|
||||||
: "";
|
: "";
|
||||||
|
|
||||||
var result = await Coordinator.RequestSummaryAndQuizAsync($"{contextPrompt}{SelectedText}");
|
_ = Coordinator.StartSelectionSummaryAsync($"{contextPrompt}{selectedText}");
|
||||||
if (result.IsSuccess)
|
|
||||||
{
|
|
||||||
await CloseAsync();
|
await CloseAsync();
|
||||||
await InteractionService.RequestAssistant();
|
await InteractionService.RequestAssistant();
|
||||||
}
|
}
|
||||||
@@ -136,18 +143,28 @@
|
|||||||
StateHasChanged();
|
StateHasChanged();
|
||||||
|
|
||||||
try
|
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)
|
var contextPrompt = !string.IsNullOrWhiteSpace(FullPageContent)
|
||||||
? $"ANALYSIS CONTEXT (Full Page Content):\n{FullPageContent}\n\nUSER SELECTION TO SUMMARIZE:\n"
|
? $"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)
|
if (result.IsSuccess)
|
||||||
{
|
{
|
||||||
await CloseAsync();
|
await CloseAsync();
|
||||||
await QuizService.RequestQuiz(BlockId);
|
await QuizService.RequestQuiz(BlockId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"[SelectionAiPanel] Error generating quiz: {ex.Message}");
|
Console.WriteLine($"[SelectionAiPanel] Error generating quiz: {ex.Message}");
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
@inject NavigationManager NavigationManager
|
@inject NavigationManager NavigationManager
|
||||||
@inject Microsoft.Extensions.Logging.ILogger<ReaderLayout> Logger
|
@inject Microsoft.Extensions.Logging.ILogger<ReaderLayout> Logger
|
||||||
@inject IThemeService ThemeService
|
@inject IThemeService ThemeService
|
||||||
|
@inject KnowledgeCoordinator Coordinator
|
||||||
@implements IAsyncDisposable
|
@implements IAsyncDisposable
|
||||||
|
|
||||||
|
|
||||||
@@ -63,7 +64,32 @@
|
|||||||
<span class="panel-title">Contextual Intelligence Panel</span>
|
<span class="panel-title">Contextual Intelligence Panel</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body">
|
<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-details">
|
||||||
<div class="node-header-section">
|
<div class="node-header-section">
|
||||||
@@ -166,7 +192,32 @@
|
|||||||
{
|
{
|
||||||
<div class="contextual-intelligence-panel">
|
<div class="contextual-intelligence-panel">
|
||||||
<div class="panel-body">
|
<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-details">
|
||||||
<div class="node-header-section">
|
<div class="node-header-section">
|
||||||
@@ -293,6 +344,7 @@
|
|||||||
InteractionService.OnScrollPercentChanged += HandleScrollPercentChanged;
|
InteractionService.OnScrollPercentChanged += HandleScrollPercentChanged;
|
||||||
GraphService.OnGraphUpdated += HandleGraphUpdatedAsync;
|
GraphService.OnGraphUpdated += HandleGraphUpdatedAsync;
|
||||||
ThemeService.OnThemeChanged += HandleThemeChangedAsync;
|
ThemeService.OnThemeChanged += HandleThemeChangedAsync;
|
||||||
|
Coordinator.OnSelectionSummaryStateChanged += HandleUpdate;
|
||||||
|
|
||||||
var context = PlatformService.GetDeviceContext();
|
var context = PlatformService.GetDeviceContext();
|
||||||
if (context.IsSuccess)
|
if (context.IsSuccess)
|
||||||
@@ -333,6 +385,11 @@
|
|||||||
StateHasChanged();
|
StateHasChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task ClearSelectionSummary()
|
||||||
|
{
|
||||||
|
await Coordinator.ClearSelectionSummaryAsync();
|
||||||
|
}
|
||||||
|
|
||||||
private async Task HandleScrollPercentChanged(int percent)
|
private async Task HandleScrollPercentChanged(int percent)
|
||||||
{
|
{
|
||||||
_scrollPercentage = percent;
|
_scrollPercentage = percent;
|
||||||
@@ -353,13 +410,25 @@
|
|||||||
{
|
{
|
||||||
if (_isMobile)
|
if (_isMobile)
|
||||||
{
|
{
|
||||||
|
if (Coordinator.IsLoadingSelectionSummary || !string.IsNullOrEmpty(Coordinator.SelectionSummary))
|
||||||
|
{
|
||||||
|
_activeMobileTab = MobileReaderTab.Concepts;
|
||||||
|
_activeTab = SidebarTab.Knowledge;
|
||||||
|
}
|
||||||
OpenAssistant();
|
OpenAssistant();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_activeMobileTab = MobileReaderTab.Concepts;
|
_activeMobileTab = MobileReaderTab.Concepts;
|
||||||
|
if (Coordinator.IsLoadingSelectionSummary || !string.IsNullOrEmpty(Coordinator.SelectionSummary))
|
||||||
|
{
|
||||||
|
_activeTab = SidebarTab.Knowledge;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
_activeTab = SidebarTab.Quiz;
|
_activeTab = SidebarTab.Quiz;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
await InvokeAsync(StateHasChanged);
|
await InvokeAsync(StateHasChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -450,6 +519,7 @@
|
|||||||
InteractionService.OnScrollPercentChanged -= HandleScrollPercentChanged;
|
InteractionService.OnScrollPercentChanged -= HandleScrollPercentChanged;
|
||||||
GraphService.OnGraphUpdated -= HandleGraphUpdatedAsync;
|
GraphService.OnGraphUpdated -= HandleGraphUpdatedAsync;
|
||||||
ThemeService.OnThemeChanged -= HandleThemeChangedAsync;
|
ThemeService.OnThemeChanged -= HandleThemeChangedAsync;
|
||||||
|
Coordinator.OnSelectionSummaryStateChanged -= HandleUpdate;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -705,3 +705,66 @@ main {
|
|||||||
background: #f9f9f9;
|
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 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>
|
/// <summary>
|
||||||
/// Raised when the knowledge graph has been updated with new data.
|
/// Raised when the knowledge graph has been updated with new data.
|
||||||
/// Subscribers must return a Task to enable proper async handling.
|
/// Subscribers must return a Task to enable proper async handling.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public event Func<GraphDataDto, Task>? OnGraphUpdated;
|
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(
|
public KnowledgeCoordinator(
|
||||||
IKnowledgeService knowledgeService,
|
IKnowledgeService knowledgeService,
|
||||||
IKnowledgeGraphService graphService,
|
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()
|
public async Task ClearAsync()
|
||||||
{
|
{
|
||||||
CancelAndDisposeCts(ref _graphCts);
|
CancelAndDisposeCts(ref _graphCts);
|
||||||
@@ -213,6 +263,7 @@ public sealed partial class KnowledgeCoordinator : IDisposable, IAsyncDisposable
|
|||||||
CurrentFullPageContent = string.Empty;
|
CurrentFullPageContent = string.Empty;
|
||||||
await _graphService.Clear();
|
await _graphService.Clear();
|
||||||
await _quizService.SetQuiz(null, null);
|
await _quizService.SetQuiz(null, null);
|
||||||
|
await ClearSelectionSummaryAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
|
|||||||
@@ -34,9 +34,11 @@ export function positionToolbar() {
|
|||||||
const relativeTop = firstRect.top - toolbarHeight - 14;
|
const relativeTop = firstRect.top - toolbarHeight - 14;
|
||||||
|
|
||||||
let top;
|
let top;
|
||||||
|
let below = false;
|
||||||
if (relativeTop < 0) {
|
if (relativeTop < 0) {
|
||||||
// Pozwól wskoczyć POD zaznaczony tekst
|
// Pozwól wskoczyć POD zaznaczony tekst
|
||||||
top = (combinedRect.bottom - canvasRect.top) + scrollTop + 12;
|
top = (combinedRect.bottom - canvasRect.top) + scrollTop + 12;
|
||||||
|
below = true;
|
||||||
toolbarElement.classList.add('below');
|
toolbarElement.classList.add('below');
|
||||||
} else {
|
} else {
|
||||||
top = (firstRect.top - canvasRect.top) + scrollTop - toolbarHeight - 14;
|
top = (firstRect.top - canvasRect.top) + scrollTop - toolbarHeight - 14;
|
||||||
@@ -45,6 +47,12 @@ export function positionToolbar() {
|
|||||||
|
|
||||||
toolbarElement.style.left = `${left}px`;
|
toolbarElement.style.left = `${left}px`;
|
||||||
toolbarElement.style.top = `${top}px`;
|
toolbarElement.style.top = `${top}px`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
left: left,
|
||||||
|
top: top,
|
||||||
|
below: below
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function initSelectionListener(dotNetHelper, container) {
|
export function initSelectionListener(dotNetHelper, container) {
|
||||||
@@ -101,3 +109,7 @@ export function initSelectionListener(dotNetHelper, container) {
|
|||||||
container.addEventListener('mouseup', () => setTimeout(handleSelection, 10));
|
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: