feat: integrate AI-driven selection panel with context-aware text summarization and quiz generation features.
This commit is contained in:
@@ -7,13 +7,17 @@
|
||||
@inject IJSRuntime JS
|
||||
@inject IFocusModeService FocusMode
|
||||
@inject IKnowledgeGraphService GraphService
|
||||
@inject IReaderInteractionService InteractionService
|
||||
|
||||
<div class="knowledge-graph-container" id="@ContainerId">
|
||||
@if (GraphData == null)
|
||||
@if (GraphService.IsLoading || GraphService.CurrentGraphData == null)
|
||||
{
|
||||
<div class="loading-state">
|
||||
<NexusIcon Name="robot" Size="48" Class="neon-glow" />
|
||||
<NexusTypography>Analyzing Chapter Nodes...</NexusTypography>
|
||||
<div class="preloader-robot">
|
||||
<NexusIcon Name="robot" Size="64" Class="neon-pulse" />
|
||||
<div class="scan-line"></div>
|
||||
</div>
|
||||
<NexusTypography Variant="NexusTypography.TypographyVariant.UI">Mapowanie relacji rozdziału...</NexusTypography>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
@@ -31,7 +35,6 @@
|
||||
[Parameter] public EventCallback<string> OnNodeSelected { get; set; }
|
||||
|
||||
private string ContainerId = "d3-graph-container";
|
||||
private GraphDataDto? GraphData;
|
||||
private IJSObjectReference? _module;
|
||||
private DotNetObjectReference<KnowledgeGraph>? _dotNetHelper;
|
||||
|
||||
@@ -40,12 +43,22 @@
|
||||
FocusMode.OnFocusModeChanged += HandleFocusSimulation;
|
||||
GraphService.OnGraphUpdated += HandleGraphUpdate;
|
||||
GraphService.OnActiveNodeChanged += HandleActiveNodeChange;
|
||||
GraphService.OnLoadingChanged += HandleLoadingChange;
|
||||
}
|
||||
|
||||
private async void HandleGraphUpdate()
|
||||
{
|
||||
if (_module == null) return;
|
||||
await _module.InvokeVoidAsync("updateData", GraphService.CurrentGraphData);
|
||||
|
||||
if (GraphService.CurrentGraphData == null)
|
||||
{
|
||||
await _module.InvokeVoidAsync("clear");
|
||||
}
|
||||
else
|
||||
{
|
||||
await _module.InvokeVoidAsync("updateData", GraphService.CurrentGraphData);
|
||||
}
|
||||
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
@@ -55,16 +68,20 @@
|
||||
await _module.InvokeVoidAsync("setActiveNode", nodeId);
|
||||
}
|
||||
|
||||
private async void HandleLoadingChange(bool isLoading)
|
||||
{
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (firstRender)
|
||||
{
|
||||
var result = await Mediator.Send(new GetKnowledgeGraphQuery());
|
||||
if (result.IsSuccess)
|
||||
await InitializeGraphAsync();
|
||||
|
||||
if (GraphService.CurrentGraphData != null)
|
||||
{
|
||||
GraphData = result.Value;
|
||||
StateHasChanged();
|
||||
await InitializeGraphAsync();
|
||||
HandleGraphUpdate();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -73,7 +90,7 @@
|
||||
{
|
||||
_module = await JS.InvokeAsync<IJSObjectReference>("import", "./_content/NexusReader.UI.Shared/js/knowledgeGraph.js");
|
||||
_dotNetHelper = DotNetObjectReference.Create(this);
|
||||
await _module.InvokeVoidAsync("mount", ContainerId, GraphData, _dotNetHelper);
|
||||
await _module.InvokeVoidAsync("mount", ContainerId, GraphService.CurrentGraphData, _dotNetHelper);
|
||||
}
|
||||
|
||||
private async Task ZoomIn() => await (_module?.InvokeVoidAsync("zoomIn") ?? ValueTask.CompletedTask);
|
||||
@@ -83,6 +100,8 @@
|
||||
[JSInvokable]
|
||||
public async Task OnNodeClicked(string nodeId)
|
||||
{
|
||||
InteractionService.NotifyNodeSelected(nodeId);
|
||||
|
||||
if (OnNodeSelected.HasDelegate)
|
||||
{
|
||||
await OnNodeSelected.InvokeAsync(nodeId);
|
||||
@@ -106,6 +125,10 @@
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
FocusMode.OnFocusModeChanged -= HandleFocusSimulation;
|
||||
GraphService.OnGraphUpdated -= HandleGraphUpdate;
|
||||
GraphService.OnActiveNodeChanged -= HandleActiveNodeChange;
|
||||
GraphService.OnLoadingChanged -= HandleLoadingChange;
|
||||
|
||||
try
|
||||
{
|
||||
if (_module is not null)
|
||||
@@ -114,14 +137,7 @@
|
||||
await _module.DisposeAsync();
|
||||
}
|
||||
}
|
||||
catch (JSDisconnectedException)
|
||||
{
|
||||
// Ignored, the circuit is already closed
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
// Ignored, the circuit is already closed
|
||||
}
|
||||
catch { }
|
||||
|
||||
_dotNetHelper?.Dispose();
|
||||
}
|
||||
|
||||
@@ -46,19 +46,50 @@
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
|
||||
.loading-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
animation: pulse 2s infinite ease-in-out;
|
||||
gap: 1.5rem;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% { opacity: 0.6; }
|
||||
50% { opacity: 1; }
|
||||
100% { opacity: 0.6; }
|
||||
.preloader-robot {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.neon-pulse {
|
||||
color: var(--nexus-neon);
|
||||
filter: drop-shadow(0 0 10px var(--nexus-neon));
|
||||
animation: robot-pulse 2s infinite ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes robot-pulse {
|
||||
0% { transform: scale(1); filter: drop-shadow(0 0 10px var(--nexus-neon)); }
|
||||
50% { transform: scale(1.1); filter: drop-shadow(0 0 25px var(--nexus-neon)); }
|
||||
100% { transform: scale(1); filter: drop-shadow(0 0 10px var(--nexus-neon)); }
|
||||
}
|
||||
|
||||
.scan-line {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background: var(--nexus-neon);
|
||||
box-shadow: 0 0 15px var(--nexus-neon);
|
||||
animation: scan 2s infinite linear;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
@keyframes scan {
|
||||
0% { top: 0; }
|
||||
50% { top: 100%; }
|
||||
100% { top: 0; }
|
||||
}
|
||||
|
||||
::deep .nexus-node-active {
|
||||
@@ -67,9 +98,3 @@
|
||||
filter: drop-shadow(0 0 12px var(--nexus-neon));
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.neon-glow {
|
||||
color: var(--nexus-neon);
|
||||
filter: drop-shadow(0 0 5px var(--nexus-neon));
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
@inject IFocusModeService FocusMode
|
||||
@inject IReaderNavigationService NavigationService
|
||||
@inject KnowledgeCoordinator Coordinator
|
||||
@inject IReaderInteractionService InteractionService
|
||||
|
||||
<div class="reader-canvas @(ThemeService.IsLightMode ? "theme-light" : "theme-dark")">
|
||||
@if (ViewModel == null)
|
||||
@@ -19,36 +20,45 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="reader-flow-container">
|
||||
<div @ref="_containerRef" class="reader-flow-container">
|
||||
@foreach (var block in ViewModel.Blocks)
|
||||
{
|
||||
<div id="@block.Id" class="block-wrapper">
|
||||
<div id="@block.Id" class="block-wrapper @(_highlightedBlockId == block.Id ? "highlighted" : "")">
|
||||
@if (block is TextSegmentBlock textSegment)
|
||||
{
|
||||
<NexusTypography Variant="NexusTypography.TypographyVariant.Ebook">@((MarkupString)textSegment.Content)</NexusTypography>
|
||||
}
|
||||
else if (block is AiActionTriggerBlock aiTrigger)
|
||||
{
|
||||
<AiAssistantBubble
|
||||
ContextBlockId="@block.Id"
|
||||
Dialogue="@aiTrigger.Dialogue"
|
||||
Actions="@aiTrigger.ActionOptions"
|
||||
OnActionTriggered="HandleAiAction" />
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<SelectionAiPanel
|
||||
SelectedText="@_selectedText"
|
||||
BlockId="@_selectedBlockId"
|
||||
Coordinates="@_selectionCoords"
|
||||
FullPageContent="@GetFullPageContent()" />
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private ReaderPageViewModel? ViewModel;
|
||||
private string StatusMessage = "Loading chapter...";
|
||||
|
||||
private string _selectedText = string.Empty;
|
||||
private string _selectedBlockId = string.Empty;
|
||||
private SelectionCoordinates? _selectionCoords;
|
||||
private string? _highlightedBlockId;
|
||||
private bool _isJsInitialized;
|
||||
private ElementReference _containerRef;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
ThemeService.OnThemeChanged += StateHasChanged;
|
||||
NavigationService.OnNavigationChanged += OnNavigationChanged;
|
||||
|
||||
InteractionService.OnScrollToBlockRequested += HandleScrollRequested;
|
||||
InteractionService.OnHighlightBlockRequested += HandleHighlightRequested;
|
||||
InteractionService.OnTextSelected += HandleTextSelected;
|
||||
}
|
||||
|
||||
protected override async Task OnParametersSetAsync()
|
||||
@@ -58,19 +68,33 @@
|
||||
|
||||
private async void OnNavigationChanged()
|
||||
{
|
||||
_isJsInitialized = false;
|
||||
_selectedText = string.Empty;
|
||||
_selectionCoords = null;
|
||||
await LoadChapterAsync(NavigationService.CurrentChapterIndex);
|
||||
StateHasChanged();
|
||||
await InitializeObserverAsync();
|
||||
}
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (firstRender)
|
||||
if (ViewModel != null && !_isJsInitialized)
|
||||
{
|
||||
_isJsInitialized = true;
|
||||
await InitializeObserverAsync();
|
||||
await InitializeSelectionListenerAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task InitializeSelectionListenerAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var module = await JS.InvokeAsync<IJSObjectReference>("import", "./_content/NexusReader.UI.Shared/js/selectionHandler.js");
|
||||
await module.InvokeVoidAsync("initSelectionListener", DotNetObjectReference.Create(this), _containerRef);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
private async Task InitializeObserverAsync()
|
||||
{
|
||||
try
|
||||
@@ -87,6 +111,49 @@
|
||||
Coordinator.OnBlockReached(blockId, content);
|
||||
}
|
||||
|
||||
[JSInvokable]
|
||||
public void HandleTextSelected(string text, string blockId, SelectionCoordinates coords)
|
||||
{
|
||||
Console.WriteLine($"[ReaderCanvas] Text selected: {text} at {coords.Top},{coords.Left}");
|
||||
_selectedText = text;
|
||||
_selectedBlockId = blockId;
|
||||
_selectionCoords = coords;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
[JSInvokable]
|
||||
public void HandleSelectionCleared()
|
||||
{
|
||||
_selectedText = string.Empty;
|
||||
_selectionCoords = null;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private void HandleScrollRequested(string blockId)
|
||||
{
|
||||
_ = ScrollToNodeAsync(blockId);
|
||||
}
|
||||
|
||||
private async void HandleHighlightRequested(string blockId)
|
||||
{
|
||||
_highlightedBlockId = blockId;
|
||||
StateHasChanged();
|
||||
await Task.Delay(3000); // Highlight for 3 seconds
|
||||
if (_highlightedBlockId == blockId)
|
||||
{
|
||||
_highlightedBlockId = null;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private string GetFullPageContent()
|
||||
{
|
||||
if (ViewModel == null) return string.Empty;
|
||||
return string.Join("\n\n", ViewModel.Blocks
|
||||
.OfType<TextSegmentBlock>()
|
||||
.Select(b => $"[ID: {b.Id}]\n{b.Content}"));
|
||||
}
|
||||
|
||||
private async Task LoadChapterAsync(int index)
|
||||
{
|
||||
ViewModel = null;
|
||||
@@ -97,6 +164,9 @@
|
||||
{
|
||||
ViewModel = result.Value;
|
||||
NavigationService.UpdateMetadata(ViewModel.CurrentChapterIndex, ViewModel.TotalChapters, ViewModel.ChapterTitle);
|
||||
|
||||
// Trigger full page graph generation after loading
|
||||
_ = Coordinator.ProcessFullPageAsync(GetFullPageContent());
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -122,5 +192,9 @@
|
||||
{
|
||||
ThemeService.OnThemeChanged -= StateHasChanged;
|
||||
NavigationService.OnNavigationChanged -= OnNavigationChanged;
|
||||
|
||||
InteractionService.OnScrollToBlockRequested -= HandleScrollRequested;
|
||||
InteractionService.OnHighlightBlockRequested -= HandleHighlightRequested;
|
||||
InteractionService.OnTextSelected -= HandleTextSelected;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,10 +3,64 @@
|
||||
margin: 0 auto;
|
||||
padding: 2rem 1rem;
|
||||
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.reader-flow-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.block-wrapper {
|
||||
transition: all 0.5s ease;
|
||||
border-radius: 8px;
|
||||
padding: 8px;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.block-wrapper.highlighted {
|
||||
background: rgba(0, 243, 255, 0.08);
|
||||
box-shadow: 0 0 20px rgba(0, 243, 255, 0.15);
|
||||
border-color: #00f3ff;
|
||||
transform: scale(1.01);
|
||||
}
|
||||
|
||||
.ai-sparkle-trigger {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
background: rgba(18, 18, 18, 0.6);
|
||||
border: 1px solid rgba(0, 255, 153, 0.3);
|
||||
border-radius: 20px;
|
||||
cursor: pointer;
|
||||
margin: 1rem 0;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.ai-sparkle-trigger:hover {
|
||||
background: rgba(0, 255, 153, 0.1);
|
||||
border-color: #00ff99;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.sparkle-tooltip {
|
||||
font-size: 0.75rem;
|
||||
color: #fff;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.neon-pulse {
|
||||
color: #00ff99;
|
||||
filter: drop-shadow(0 0 5px #00ff99);
|
||||
animation: pulse-small 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse-small {
|
||||
0% { transform: scale(1); opacity: 1; }
|
||||
50% { transform: scale(1.1); opacity: 0.8; }
|
||||
100% { transform: scale(1); opacity: 1; }
|
||||
}
|
||||
Reference in New Issue
Block a user